|
58663
|
2086
|
0
|
2026-05-19T13:23:15.795846+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779196995795_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.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...
|
[{"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}]...
|
-4265427748691914896
|
-9066868193213567546
|
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
SlackFileEditViewGoHistoryWindowHelpAPP(useDOCKER₴81DEV (docker)₴82"gitrestore ‹file>..."todiscard changesin working directory)modified:.env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.phpAPP (-zs!Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.php1iriPS3$1riri31FrSlack16tayS/2UC..045aD<e1/48a03ca9e03..84e7b10765JT-<084<-remove-elastic-search-delete-documents->JY-20943-fix-calendar-eventsorigin/JT-4origin/JY-20Updating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.php48app/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php0app/Component/Transcription/Service/StorageService.phpfront-end/package.jsonfront-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-emaiHomeDMsActivityED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-vousNAneliya Angelovaa. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanov2. James GrahamStoyan Tanev. Lukas Kovalik y...::: Apps|i Jira CloudToast100% <78• Tue 19 May 16:23:15QDescribe what you are looking for# releases8 22• MessagesC Files• Bookmarks+177ea29b - Mequest #12102 fromTodayjiminny/JY-209~+ un u ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02LOLUIUULI LIVIVAZUNUUUUNINI VUII LUICALI[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...
|
58660
|
NULL
|
NULL
|
NULL
|
|
58662
|
NULL
|
0
|
2026-05-19T13:23:05.982655+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779196985982_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.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...
|
[{"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}]...
|
-4265427748691914896
|
-9066868193213567546
|
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
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reroledey© InternetMessagelnterface.php© MailChannelService.phpD MeetingGeneratoruNotricationWOAUtnZD RecallAW Telephonvl User?ilotWeohook© AbstractService.php© ActivityProviderFactory.php© ActivityService.php© ApiResponseService.php@ ConferenceService.php(c) InciahtSoatService nhn© InstantMeetingService.php© IntercomService.php© IpapiClient.php@)InapiService.php© ParticipantShareService.php@ DlanhatService.onp© PlaybackService.php(© PlaybackVideoOnlyService.php© PlaybookCategoryService.php• PlaylistGeneratorInterface.phpc) Resolveleamermeonnection.php(c) SimpleThrottleservice.phc(c) SlackService.phc© SocialAccountService.phpc) SortPhoneService.ohr@ TeamDeactivatedService.ohv(C) [EMAIL]@ TeamService.php(C) TranscodeParameterResolver.oho@ UserService.ohn(C) Uuid.nho> D Traits• MUseCases> MUserMUtils• M Validationi•MVophp helpers.php© InitialFrontendState.php(e) liminny nhn(e) Dian nhn© ConvertLeadActivities.php(C) SaveActivity.pnp© SyncPlanhat.phpo synerorlannatpnp© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php(C) AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;BI4KLAYuse Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository,private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoni.v AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lani/v1lautomated-renorts/interest 1usaaeLalu)public function trackInterest(Request Srequest): JsonResponset...}* Othnows ApplicationExceptionGET /api/v1/automated-reports58(09 >public function list(Request Srequest): JsonResponse(...}125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]# console [PKol)© TextRelayService.php x = .env.production-eu© WebhookController.phpA console (EU]A console [STAGING]stohenamespace Jininny services Marl>use..•class Textrelayservicepublic function constructor...* Fetch the latest messaaes since the last sunc..public function sync): arraySmailbox = config( key: 'jiminny.google_text_user'):SexpectedRecipient = Smailbox .'@' . config( key:"jiminny.google_text_host'):AccentRejectScenvice = Sthis->ae+Senvice(Smailhoxa•$messageHistory = $this->getHistory(Sservice):$messageIds = 0]:foreach (SmessageHistory as Shistories) {Smocconos = Chictonioc-moccanocAddod.foreach (Smessages as Smessage)smessageld = smessaqe->messaqe->10*if (!Sthis->isForCurrentEnvironment($service, $mailbox, Smessageld. SexpectedRecipient)) {SrelavedText = TextRelay:: where, colun'email provider id', Smessageld)->firsto:if (Srelavedtext === nulbSrelavedText = TextRelav::createdliTextRelav: : PROVIDER GSUMTEI=> TextRelav•• STATUIS pROcesStNG..Sjob = (new EmailTextRelay(SmessageId, SrelayedText))->onQueue( queue: Constants: :QUEUE_EMAILS),dicnatchl Caoh)"SmocconoTdell - SmoccanoTd.Accept File &- X Reject File +&g+ 1of2 filles →100% 5• Tue 19 May 16:23:05AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Rout+0 ..+1-1Thoughts9/10 tasks done• Revert.env.production-eu domain change1 Explain complete text relay flowmolementation comoleteChanges Made:1. TextRelavService,ohv - Email-based filterina• Uodated synco to filter by full email address instead of iust domain• Added isForCurrentEnvironment() method to check if email recipient matches the environment's configured mailbox• Comoares tullrecipient: catch-allatxt.iiminnv.com vs catch-all-eudtxt.timinny.com2. .env.production-eu - Mailbox change• Changed GOOGLE_TEXT_RELAY_MAILBOX from catch-all to catch-all-eu• Kept GOOGLE TExT RELAY_HOST as txt.jiminny.com no new domain needed)Setup Required,• Create Gmail emaill address: catch-al-euatxt.iminny.com• US uses: [EMAIL]•=uuses: catch-al-euatxt.liminny.comComplete Text Relav FlowPhase 1: SMS Received → Email Notification1 Tiwilia Sandc SMS|• User receives SMS from nrosnect via Twiliol• Twilio delivers SMS to prospect's phone2. Twilio Webhook Triaaeredi• Twilio sends webhook to: posT /ani/v1//webhook/twi.110/smsController: WebhookController@handLesms• File:aoo/Htto/Controllers/Teleohony/TextMessagina/WebhookController.oho2liminny Drocaccec Wehhook• Webhook payload contains: From, To, Body, MessageSid• Svstem finds the activity associated with the phone number• Disoatches SmclReceived event4. Event Listener: CreateReceivedNotification• File: app/Listeners/Activities/Sms/CreateReceivedNotification.php• Marks activitv as processedAccent alliAsk anvthing 884-D)« Code SWF-1.6WN Windsurf Toams 28-1UTF.8io 4 spaces...
|
58661
|
NULL
|
NULL
|
NULL
|
|
58661
|
2085
|
5
|
2026-05-19T13:23:02.004323+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779196982004_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-a PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reProiect© RecipientServiceTest.php>D Mail> D MeetingGenerator>D Notification• U kecallAI>C Team>C Telephony?DUser>W UserPilotWebhook©) ActivityServiceTest.phoC) ApiResponseServicetest.phoc) InstantMeetingServicetest.oho@ IntercomServiceTest.ohnc KioskServiceTest.ohoc [EMAIL]© PlaybookCategoryServiceTest.php© ResolveTeamCrmConnectionTest.php( SlackService Test nhn© SocialAccountServiceTest.phpC TeamDeactivatedServiceTest.php© TeamOwnerServiceTest.php(* TeamServiceTest.php© UserServiceTest.php> D TraitsD UseCases> C UserC UtilsC Validation0 EnumInArrayTrait.php(© HelpersTest.php@ InitialFrontendStateTest.phpMockunusedDevendencies.rait.ono> Munit2022(t ApiGuardLoain.ohv(* TestCase.ohn> Mvendoro editorcontialE .env= env circieciE .env.circleci-nightly= envlocal= env miarate= env nikilocallE.env.otheE env nroduction=ony nroduction-otE.env.qa.env.qaConvertLeadActivities.php(C) SaveActivity.pnp© SyncPlanhat.phpo synerorlannatpnp© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phgC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php(C)AutomatedReportResult.oho(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;use Jiminny \Services\PlanhatService;BI4KLAYuse ectumtnace hutp kequestuse Inrowable,class userauconacedreporcsconcrocler excenos controutepublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoni.v AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lani/v1lautomated-renorts/interest 1usaaeLalu)public function trackInterest(Request Srequest): JsonResponset...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponsef...}=custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextRelayService.php x = .env.production-eu© WebhookController.phpA console (EU]A console [STAGING]stohenamespace Jininny services Marl>use..•class Textrelayservicepublic function constructor...* Fetch the latest messaaes since the last sunc..public function sync): arraySmailbox = config( key: 'jiminny.google_text_user'):SexpectedRecipient = Smailbox .'@' . config( key:"jiminny.google_text_host'):AccentRejectScenvice = Sthis->ae+Senvice(Smailhoxa•$messageHistory = $this->getHistory(Sservice):$messageIds = 0]:foreach (SmessageHistory as Shistories) {Smocconos = Chictonioc-moccanocAddod.foreach (Smessages as Smessage)smessageld = smessaqe->messaqe->10*if (!Sthis->isForCurrentEnvironment($service, $mailbox, Smessageld. SexpectedRecipient)) {SrelavedText = TextRelav:: where colun'email provider id', Smessageld)->firsto:if (Srelavedtext === nulbSrelavedText = TextRelav::createdliTextRelav: : PROVIDER GSUMTEI=> TextRelav•• STATUIS pROcesStNG..Sjob = (new EmailTextRelay(SmessageId, SrelayedText))-›onQueue( queue: Constants::QUEUE_EMAILS),dicnatchl Caoh)"SmocconoTdell - SmoccanoTd.Accept File &- X Reject File +&g+ 1of2 filles →100% 5• Tue 19 May 16:23:01AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Routt+0 ..env.oroduction-eu+1 -1|9/10 tasks done© kevert .env.production-eu domain change10 explain complete text relay tlowImplementation CompleteChanges Made:1. TextRelayService.php - Email-based filtering• Updated sync() to filter by full email address instead of just domain• Added isForCurrentEnvironment () method to check if email recipient matches the environment's configured mailbox• Compares full recipient: [EMAIL] vs [EMAIL]. .env.production-eu - Mailbox change• Changed GOOGLE TEXT RELAY MAILBOX trom catch-all to catch-all-eu• Keot GooGLE TEXT RELAY HOST as txt.jiminnv.com no new domain needed)Setup Required:• Create Gmail email address: [EMAIL]• USuses: catch-allatxt.iiminnv.comCulucoc. catch-all_euatyt.iiminnv.comComplete Text Relay FlowDhace 1. SMS Peceived & Email Notificatior1. Twilio Sends SMS• User receives SMS from prospect via Twilio• Twilio delivers SMS to prospect's phone2. Twilio Webhook Triggered• Twilio sends webhook to: POST /api/v1/webhook/twilio/sms• Controller: WebhookControllerahandleSmslSile: ann/Httn/Controllers/Teleohony/TextMessaqina/Webhookcontrouler.pnp3. Jiminny Processes Webhook• Webhook payload contains: From. To. Body . MessageSid• Svctem finds the activitv accociated with the nhone number• Dispatches Sms\Received eventMorke ontivity de nrassccodAccent alliAsk anvthina (884-L)« Code SWF-1.6WN Windsurf Toams 28-1UTF.8Po 4 spaces...
|
NULL
|
7304648769442032445
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-a PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reProiect© RecipientServiceTest.php>D Mail> D MeetingGenerator>D Notification• U kecallAI>C Team>C Telephony?DUser>W UserPilotWebhook©) ActivityServiceTest.phoC) ApiResponseServicetest.phoc) InstantMeetingServicetest.oho@ IntercomServiceTest.ohnc KioskServiceTest.ohoc [EMAIL]© PlaybookCategoryServiceTest.php© ResolveTeamCrmConnectionTest.php( SlackService Test nhn© SocialAccountServiceTest.phpC TeamDeactivatedServiceTest.php© TeamOwnerServiceTest.php(* TeamServiceTest.php© UserServiceTest.php> D TraitsD UseCases> C UserC UtilsC Validation0 EnumInArrayTrait.php(© HelpersTest.php@ InitialFrontendStateTest.phpMockunusedDevendencies.rait.ono> Munit2022(t ApiGuardLoain.ohv(* TestCase.ohn> Mvendoro editorcontialE .env= env circieciE .env.circleci-nightly= envlocal= env miarate= env nikilocallE.env.otheE env nroduction=ony nroduction-otE.env.qa.env.qaConvertLeadActivities.php(C) SaveActivity.pnp© SyncPlanhat.phpo synerorlannatpnp© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phgC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php(C)AutomatedReportResult.oho(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;use Jiminny \Services\PlanhatService;BI4KLAYuse ectumtnace hutp kequestuse Inrowable,class userauconacedreporcsconcrocler excenos controutepublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoni.v AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lani/v1lautomated-renorts/interest 1usaaeLalu)public function trackInterest(Request Srequest): JsonResponset...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponsef...}=custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextRelayService.php x = .env.production-eu© WebhookController.phpA console (EU]A console [STAGING]stohenamespace Jininny services Marl>use..•class Textrelayservicepublic function constructor...* Fetch the latest messaaes since the last sunc..public function sync): arraySmailbox = config( key: 'jiminny.google_text_user'):SexpectedRecipient = Smailbox .'@' . config( key:"jiminny.google_text_host'):AccentRejectScenvice = Sthis->ae+Senvice(Smailhoxa•$messageHistory = $this->getHistory(Sservice):$messageIds = 0]:foreach (SmessageHistory as Shistories) {Smocconos = Chictonioc-moccanocAddod.foreach (Smessages as Smessage)smessageld = smessaqe->messaqe->10*if (!Sthis->isForCurrentEnvironment($service, $mailbox, Smessageld. SexpectedRecipient)) {SrelavedText = TextRelav:: where colun'email provider id', Smessageld)->firsto:if (Srelavedtext === nulbSrelavedText = TextRelav::createdliTextRelav: : PROVIDER GSUMTEI=> TextRelav•• STATUIS pROcesStNG..Sjob = (new EmailTextRelay(SmessageId, SrelayedText))-›onQueue( queue: Constants::QUEUE_EMAILS),dicnatchl Caoh)"SmocconoTdell - SmoccanoTd.Accept File &- X Reject File +&g+ 1of2 filles →100% 5• Tue 19 May 16:23:01AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Routt+0 ..env.oroduction-eu+1 -1|9/10 tasks done© kevert .env.production-eu domain change10 explain complete text relay tlowImplementation CompleteChanges Made:1. TextRelayService.php - Email-based filtering• Updated sync() to filter by full email address instead of just domain• Added isForCurrentEnvironment () method to check if email recipient matches the environment's configured mailbox• Compares full recipient: [EMAIL] vs [EMAIL]. .env.production-eu - Mailbox change• Changed GOOGLE TEXT RELAY MAILBOX trom catch-all to catch-all-eu• Keot GooGLE TEXT RELAY HOST as txt.jiminnv.com no new domain needed)Setup Required:• Create Gmail email address: [EMAIL]• USuses: catch-allatxt.iiminnv.comCulucoc. catch-all_euatyt.iiminnv.comComplete Text Relay FlowDhace 1. SMS Peceived & Email Notificatior1. Twilio Sends SMS• User receives SMS from prospect via Twilio• Twilio delivers SMS to prospect's phone2. Twilio Webhook Triggered• Twilio sends webhook to: POST /api/v1/webhook/twilio/sms• Controller: WebhookControllerahandleSmslSile: ann/Httn/Controllers/Teleohony/TextMessaqina/Webhookcontrouler.pnp3. Jiminny Processes Webhook• Webhook payload contains: From. To. Body . MessageSid• Svctem finds the activitv accociated with the nhone number• Dispatches Sms\Received eventMorke ontivity de nrassccodAccent alliAsk anvthina (884-L)« Code SWF-1.6WN Windsurf Toams 28-1UTF.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58660
|
NULL
|
0
|
2026-05-19T13:22:59.119891+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779196979119_m1.jpg...
|
PhpStorm
|
faVsco.js – .env.production-eu
|
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...
|
[{"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}]...
|
-1900268502525982722
|
10344525981010375
|
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
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (docker)₴82APP (-zslCuse"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 GrahamStoyan TanevE Lukas Kovalik y... 0::: Appsi Jira CloudToast100% <Tue 19 May 16:22:59Describe 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
|
|
58609
|
2079
|
8
|
2026-05-19T13:00:49.633510+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195649633_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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
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":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"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.24740623,"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\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":1.0},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","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":"17","depth":4,"bounds":{"left":0.70113033,"top":0.10055866,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.28756648,"height":0.8818835},"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":"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}]...
|
3978042121848121570
|
-7843132677320234491
|
idle
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
58607
|
NULL
|
NULL
|
NULL
|
|
58608
|
2078
|
6
|
2026-05-19T13:00:40.768515+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195640768_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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
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":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","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":"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":"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}]...
|
3978042121848121570
|
-7843132677320234491
|
idle
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
58606
|
NULL
|
NULL
|
NULL
|
|
58607
|
2079
|
7
|
2026-05-19T13:00:19.294504+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195619294_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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
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":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"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.24740623,"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\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":1.0},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","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":"17","depth":4,"bounds":{"left":0.70113033,"top":0.10055866,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.28756648,"height":0.8818835},"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":"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}]...
|
3978042121848121570
|
-7843132677320234491
|
idle
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58606
|
2078
|
5
|
2026-05-19T13:00:10.475293+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195610475_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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
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":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","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":"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":"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}]...
|
3978042121848121570
|
-7843132677320234491
|
idle
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58605
|
2079
|
6
|
2026-05-19T12:59:48.973707+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195588973_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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder...
|
[{"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":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"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.24740623,"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\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":1.0},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","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":"17","depth":4,"bounds":{"left":0.70113033,"top":0.10055866,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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,"bounds":{"left":0.4481383,"top":0.09736632,"width":0.28756648,"height":0.8818835},"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":"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},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
5983292090688154014
|
-8996054181927081467
|
idle
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder...
|
58603
|
NULL
|
NULL
|
NULL
|
|
58604
|
2078
|
4
|
2026-05-19T12:59:40.174829+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195580174_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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
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":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","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":"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":"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}]...
|
3978042121848121570
|
-7843132677320234491
|
idle
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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 https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Failed($activity, $errorCode, $errorMessage));
Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_SENT:
$activity->update([
'status' => Activity::STATUS_SENT,
'is_processed' => true,
]);
event(new Sent($activity));
Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_DELIVERED:
$activity->update([
'status' => Activity::STATUS_DELIVERED,
]);
event(new Delivered($activity));
Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);
break;
case self::EVENT_SMS_UNDELIVERED:
$activity->update([
'status' => Activity::STATUS_UNDELIVERED,
]);
// See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages
event(new Undelivered($activity, $errorCode, $errorMessage));
break;
}
return response()->twiml();
}
public function notification(): void
{
$this->validateWebhookRequest();
$sid = $this->request->input('MessageSid');
$status = $this->request->input('MessageStatus');
if (\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {
\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
58600
|
NULL
|
NULL
|
NULL
|
|
58603
|
2079
|
5
|
2026-05-19T12:59:17.368935+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195557368_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...
|
[{"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}]...
|
-4936557604516829011
|
-9069119992792502842
|
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
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-rroledeyc) MailboxController.onp©messagecontroller.onpg weonookcontroller.onp› D Voice) Audiocontroller.onpc basecontroller.onov DWebhookHubspot›D IntearationAppsubscriptionsC) ActivityProviderController.phoC) ActivitvTranscriptioncontroller.phoc) BaseController.ohpP CalendarController.ohoc) Revortcontrolller.onoP SoftohoneWebhookController.ohpC AbstractController.oho© CommentContextinterface.php© ConferencesOptinOutController.php(c) Controller nhn© ExportController.php© FrontendController.phpT FrontendControllerTrait.php8 GeocodingController.phpc) HealthCheckcontroller.ong92 liveCoachController.php© MissingTeamController.php(e) MobileController.pno(e) NotificationController.onp© NotificationProviderController.phpPlaybackController.php© PlaylistController.php© PusherController.phpSlackController.php@ SupportController.phpc)Teamsetuocontroller.ono© UserAutomatedReportsController.phpC) WelcomeController.oho> M MiddlewareRequestsResourcesM ResnonsasM SerializersM Transformers(C) Kernel.nhnl0 PlaylistTrackResourceTrait.phpT. ValidateCrmConnectionRequiredTrait nhn.> M intearationsM intoractioncvM lohe• MActivitvM AiAntomation© ConvertLeadActivities.php© SaveActivity.php© SyncPlanhat.phpo synerorlannatpnp© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.php)Inteqrationapp/service.onoC) LeadConverted.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.phpC) AutomatedReportsRepository.php(C) AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;BI4NLAYuse Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class userauconacedreporesconcrocler excenas controutepublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTTON = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository,private readonly AutomatedRenortsService sautomatedRenortsServicelprivate readonly ApiResponseService $apiResponseService,orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService,1...poST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request Srequest): JsonResponset...}* Othnows ApplicationExceptionGET /api/v1/automated-reports58(09 >public function list(Request Srequest): JsonResponse(...}DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]# console [PKol)© TextRelayService.php= .env.production-eu© WebhookController.php X A console (EU]A console [STAGING]k?phpnamespace Jininny huup controllerslelepnonylexchessaging;>use...class webhookcontrouler extends Basecontroulen34(09 >100 >105 U>187188 U1991usagepublic const strina EVENT SMS QUEUED = 'queued' •public const string EVENT SMS FATLED = "failed'auscloepublic const string EVENT_SMS_SENT = 'sent';public const string EVENT_SMS_DELIVERED = 'delivered';public const string EVENT_SMS_UNDELIVERED = 'undelivered':* areturn mixedDAST lwshbonklemelronaivdpublic function receive(TextMessagingService SmessagingService){...}Z usagesprivate function fetchActivity@: ?Activity{...}PosT/webhook/sms/event Iiiimoublic function eventor...;POST /webhook/sms/notificationpublic function notificationo: void....amA17.v100% 5• Tue 19 May 15:59:16AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Routi+0 ..Thoughts9 10 tasks dor© kevert .env.produetion"eu domain change4 Explain complete text relay flowImplementation CompleteChanges Made1. TextRelayService.php - Email-based filtering• Uodated svnc(o) to filter bv full email address instead of iust domaini• Added 1sForcurrentEnvironment method to check it emall recipient matches the environment's contigured mallbox• Comoares fullrecioient: catch-allatxt.jiminnv.com vs catch-all-euatxt.iiminnv.com2. .env.production-eu - Mailbox change• Changed GOOGLE TEXT RELAY MAILBOX from catch-all to catch-all-eu• Keot Go0GLE TEXT RELAY HOST as txt.liminny.com no new domain needed))Setup Required:• Create Gmail email address: [EMAIL]• Us uses: [EMAIL]• EUuses: catch-all-euatxt.iiminnv.comComplete Text Relay FlowPhase 1: SMS Received → Email NotificationI1. Twilio Sends SMS• User receives SMS from prospect via Twilio• Twilio delivers SMS to prospect's phone2. Twilio Webhook Triggered• Twilio sends webhook to: POST /api/v1/webhook/twilio/sms• Controller: WebhookController@handleSms• File: app/Http/Controllers/Telephony/TextMessaging/WebhookController.php3. Jiminny Processes Webhook• Webhook payload contains: From, To, Body. MessageSidSvstem finds the activitv ascociated with the nhone numbeDienstahoe Cmel pocatiuod ovont4. Event Listener: CreateReceivedNotification• File:app/Listeners/Activities/Sms/CreateReceivedNotification.ohoMarke activity as nrocesced• Sends SmsReceived notification to user5. SmcPeceived Notification* Reiect allAccent alliAsk anvthina (884-L)« Code SWF-1.6W Windsurf Teamc1-1UTE.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58602
|
2079
|
4
|
2026-05-19T12:59:14.076612+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195554076_m2.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-a PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reProiect() RecipientServicelest.png> M MailliMeetingGenerator>D NotificationIm PecallAl>D Team> @ Telephony>D User@ userPilot>@ Webhook© ActivityServiceTest.php© ApiResponseServiceTest.phpc) Instanumeeunoservice l est.oho° IntercomServiceTest.phpc) KioskServiceTest.ohoc) PlanhatServiceTest.phoC PlavbackVideoOnIvServiceTest.phpc) PlavbookCategorvServiceTest.ohoc)ResolveTeamermconnectionirest.onoc) SackServiceTest.ono@a SocialAccountServiceTest.phpcTeamDeactivatedServiceTest.ohrC* TeamOwnerServiceTest.ohn© TeamServiceTest.phpc) UserServiceTest.ohoM Traitsm ucecacesmlicerD UtilsD ValidationT EnuminArrayTrait.phpE HelpersTest.php9t InitialFrontendStateTest.php(© MockUnusedDependenciesTrait.php> D unit2022©ApiGuardLogin.php© TestCase.php>C vendor{ô .editorconfid= envE env.circlecilenv.circleci-niahtlv= env.local= env miarate= env.nikilocall= env.other= env nroduction=.env.ga= env aa= env rontConvertLeadActivities.php© SaveActivity.phpo synerorlannatpnp© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phgC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php(C) AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php(C)AutomatedReportResult.oho(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;use Jiminny \Services\PlanhatService;BI4KLAYuse ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllerpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTTON = "sort direction':30 0,public function construct.private readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lani/v1lautomated-renorts/interest 1usaaeLalu)public function trackInterest(Request Srequest): JsonResponset...}* Othnows ApplicationException58(09 >GET /api/v1/automated-report:public function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]# console [PKol)© TextRelayService.php= .env.production-eu© WebhookController.php X A console (EU]A console [STAGING]k?phpnamespace Jininny huup controllerslelepnonylexchessaging;>use...class webhookcontrouler extends Basecontroulen34(09 >100 >105 U>187188 U1991usagepublic const strina EVENT SMS QUEUED = 'queued' •public const string EVENT SMS FATLED = "failed'auscloepublic const string EVENT_SMS_SENT = 'sent';public const string EVENT_SMS_DELIVERED = 'delivered';public const string EVENT_SMS_UNDELIVERED = 'undelivered':* areturn mixedDAST lwshbonklemelronaivdpublic function receive(TextMessagingService SmessagingService){...}Z usagesprivate function fetchActivity@: ?Activity{...}PosT/webhook/sms/event Iiiimoublic function eventor...;POST /webhook/sms/notificationpublic function notificationo: void....a100% 5• Tue 19 May 15:59:13AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Routt+0 ..9 10 tasks dor© kevert .env.produetion"eu domain change4 Explain complete text relay flowImplementation CompleteChanges Made1. TextRelayService.php - Email-based filtering• Uodated svnc(o) to filter bv full email address instead of iust domaini• Added 1sForcurrentEnvironment method to check it emall recipient matches the environment's contigured mallbox• Comoares fullrecioient: catch-allatxt.jiminnv.com vs catch-all-euatxt.iiminnv.com2. .env.production-eu - Mailbox change• Changed GOOGLE TEXT RELAY MAILBOX from catch-all to catch-all-eu• Keot Go0GLE TEXT RELAY HOST as txt.liminny.com no new domain needed))Setup Required:• Create Gmail email address: [EMAIL]• Us uses: [EMAIL]• EUuses: catch-all-euatxt.iiminnv.comComplete Text Relay FlowPhase 1: SMS Received → Email Notificationl1. Twillio Sends SMS• User receives SMS from prospect via Twilio• Twilio delivers SMS to prospect's phone2. Twilio Webhook Triggered• Twilio sends webhook to: POST /api/v1/webhook/twilio/sms• Controller: WebhookController@handleSms• File: app/Http/Controllers/Telephony/TextMessaging/WebhookController.php3. Jiminny Processes Webhook• Webhook payload contains: From, To, Body. MessageSidSvstem finds the activitv ascociated with the nhone numbeDienstahoe Cmel pocatiuod ovont4. Event Listener: CreateReceivedNotification• File:app/Listeners/Activities/Sms/CreateReceivedNotification.ohoMarke activity as nrocesced• Sends SmsReceived notification to user5. SmcPeceived Notification* Reiect allAccent alliAsk anvthina (884-L)« Code SWF-1.6W Windsurf Teamc1-1.UTE.9io 4 spaces...
|
NULL
|
-6505727359571178175
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-a PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reProiect() RecipientServicelest.png> M MailliMeetingGenerator>D NotificationIm PecallAl>D Team> @ Telephony>D User@ userPilot>@ Webhook© ActivityServiceTest.php© ApiResponseServiceTest.phpc) Instanumeeunoservice l est.oho° IntercomServiceTest.phpc) KioskServiceTest.ohoc) PlanhatServiceTest.phoC PlavbackVideoOnIvServiceTest.phpc) PlavbookCategorvServiceTest.ohoc)ResolveTeamermconnectionirest.onoc) SackServiceTest.ono@a SocialAccountServiceTest.phpcTeamDeactivatedServiceTest.ohrC* TeamOwnerServiceTest.ohn© TeamServiceTest.phpc) UserServiceTest.ohoM Traitsm ucecacesmlicerD UtilsD ValidationT EnuminArrayTrait.phpE HelpersTest.php9t InitialFrontendStateTest.php(© MockUnusedDependenciesTrait.php> D unit2022©ApiGuardLogin.php© TestCase.php>C vendor{ô .editorconfid= envE env.circlecilenv.circleci-niahtlv= env.local= env miarate= env.nikilocall= env.other= env nroduction=.env.ga= env aa= env rontConvertLeadActivities.php© SaveActivity.phpo synerorlannatpnp© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phgC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php(C) AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php(C)AutomatedReportResult.oho(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;use Jiminny \Services\PlanhatService;BI4KLAYuse ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllerpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTTON = "sort direction':30 0,public function construct.private readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lani/v1lautomated-renorts/interest 1usaaeLalu)public function trackInterest(Request Srequest): JsonResponset...}* Othnows ApplicationException58(09 >GET /api/v1/automated-report:public function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]# console [PKol)© TextRelayService.php= .env.production-eu© WebhookController.php X A console (EU]A console [STAGING]k?phpnamespace Jininny huup controllerslelepnonylexchessaging;>use...class webhookcontrouler extends Basecontroulen34(09 >100 >105 U>187188 U1991usagepublic const strina EVENT SMS QUEUED = 'queued' •public const string EVENT SMS FATLED = "failed'auscloepublic const string EVENT_SMS_SENT = 'sent';public const string EVENT_SMS_DELIVERED = 'delivered';public const string EVENT_SMS_UNDELIVERED = 'undelivered':* areturn mixedDAST lwshbonklemelronaivdpublic function receive(TextMessagingService SmessagingService){...}Z usagesprivate function fetchActivity@: ?Activity{...}PosT/webhook/sms/event Iiiimoublic function eventor...;POST /webhook/sms/notificationpublic function notificationo: void....a100% 5• Tue 19 May 15:59:13AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Routt+0 ..9 10 tasks dor© kevert .env.produetion"eu domain change4 Explain complete text relay flowImplementation CompleteChanges Made1. TextRelayService.php - Email-based filtering• Uodated svnc(o) to filter bv full email address instead of iust domaini• Added 1sForcurrentEnvironment method to check it emall recipient matches the environment's contigured mallbox• Comoares fullrecioient: catch-allatxt.jiminnv.com vs catch-all-euatxt.iiminnv.com2. .env.production-eu - Mailbox change• Changed GOOGLE TEXT RELAY MAILBOX from catch-all to catch-all-eu• Keot Go0GLE TEXT RELAY HOST as txt.liminny.com no new domain needed))Setup Required:• Create Gmail email address: [EMAIL]• Us uses: [EMAIL]• EUuses: catch-all-euatxt.iiminnv.comComplete Text Relay FlowPhase 1: SMS Received → Email Notificationl1. Twillio Sends SMS• User receives SMS from prospect via Twilio• Twilio delivers SMS to prospect's phone2. Twilio Webhook Triggered• Twilio sends webhook to: POST /api/v1/webhook/twilio/sms• Controller: WebhookController@handleSms• File: app/Http/Controllers/Telephony/TextMessaging/WebhookController.php3. Jiminny Processes Webhook• Webhook payload contains: From, To, Body. MessageSidSvstem finds the activitv ascociated with the nhone numbeDienstahoe Cmel pocatiuod ovont4. Event Listener: CreateReceivedNotification• File:app/Listeners/Activities/Sms/CreateReceivedNotification.ohoMarke activity as nrocesced• Sends SmsReceived notification to user5. SmcPeceived Notification* Reiect allAccent alliAsk anvthina (884-L)« Code SWF-1.6W Windsurf Teamc1-1.UTE.9io 4 spaces...
|
58601
|
NULL
|
NULL
|
NULL
|
|
58601
|
2079
|
3
|
2026-05-19T12:59:08.417868+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195548417_m2.jpg...
|
PhpStorm
|
faVsco.js – .env.production-eu
|
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
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reProiect() RecipientServicelest.png> M MailliMeetingGenerator>D NotificationIm PecallAl>D Team> @ Telephony>D User@ userPilot>@ Webhook©) ActivityServiceTest.php© ApiResponseServiceTest.phpc) Instanumeeunoservice l est.oho° IntercomServiceTest.phpc) KioskServiceTest.ohoc) PlanhatServiceTest.phoC PlavbackVideoOnIvServiceTest.phpc) PlavbookCategorvServiceTest.ohoc)ResolveTeamermconnectionirest.onoc) SackServiceTest.ono@a SocialAccountServiceTest.phpcTeamDeactivatedServiceTest.ohrC* TeamOwnerServiceTest.ohn© TeamServiceTest.phpc) UserServiceTest.ohoM Traitsm ucecacesmlicerD UtilsD ValidationT EnuminArrayTrait.phpE HelpersTest.php9t InitialFrontendStateTest.php(© MockUnusedDependenciesTrait.php> D unit2022©ApiGuardLogin.php© TestCase.php>C vendor{ô .editorconfid= envE env.circlecilenv.circleci-niahtlv= env.local= env miarate= env.nikilocall= env.other= env nroduction=.env.ga= env aa= env rontConvertLeadActivities.php(C) SaveActivity.pnp© SyncToPlanhat.php© AcuvityLeadConvertea.php© UserAutomatedreporscontroller.phpx© PlaybackController.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phgC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php(C) AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php(C)AutomatedReportResult.oho(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;use Jiminny \Services\PlanhatService;A14X2 A Vuse eLtumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':30 0,public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...kalusspoST lani/v1lautomated-renorts/interest 1usaaepublic function trackInterest(Request Srequest): JsonResponsef...}* Othnows ApplicationException58(09 >GET /api/v1/automated-report:public function list(Request Srequest): JsonResponse(...}125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponsef...}=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextRelayService.php _269270271272273274275E .env.production-eu X A console [EU]A console [STAGING]FFPROBE_PATH=/usr/local/bin/ffprobeFFMPEG_PATH=/usr/local/bin/ffmpegSOS [ENV_SECRET] AWS_SECRET_KEY= QUEUE [ENV_SECRET] QUEUE= QUEUE SOS_ NAMESQS_FIFO_QUEUE=_QUEUE_SQS_FIFO_NAME.FIFOSOS_REGION= QUEUE_REGIONKINESTS AWS REGTON=eU-west-1nstchis Events aws_kanesis streamNAMEzactivitv-event-insiahtslINSIGHTS METRICS AWS KINESTS STREAM_NAME=activitv-metric-insiahtsGOOGLE TEXT_RELAY_MAILBOX=catch-allGOOGLE_TEXT_RELAY_MAILBOX=catch-all-euGOOGLE_TEXT_RELAY_HOST=txt.jiminny.comGOOGLE_TEXT_RELAY_TOPIC=projects/jiminny-web/topics/com.jiminny.txtGOOGLE_TEXT_RELAY_SUBSCRIPTION=proiects/jiminny-web/subscriptions/relayELASTICSEARCH_HOST= AWS_ ELASTIC SEARCH_HOSTELASTICSEARCH_PORT=80ELASTICSEARCH_TRANSPORT=HttpCDN UR[URL_WITH_CREDENTIALS] File:aon/Httn/Controllers/Telenhonv/TextMessaqina/WehhookController.nho3. Jiminny Processes Webhook• Webhook payload contains: From, To, Body. MessageSid• Svstem finds the activitv ascociated with the nhone number.Dienstahoe Cmel pocatiuod ovont4. Event Listener: CreateReceivedNotification• File:app/Listeners/Activities/Sms/CreateReceivedNotification.ohoMarks activity as processed• Sends SmsReceived notification to user5. SmcPeceived Notification* Reiect allAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 248-1UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58600
|
2078
|
3
|
2026-05-19T12:59:08.390820+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195548390_m1.jpg...
|
PhpStorm
|
faVsco.js – .env.production-eu
|
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...
|
[{"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}]...
|
-7665217052062073462
|
-8634242253287084858
|
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
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp• •APP (-zsh)APP (-zsh)DOCKER• ₴1DEV (docker)₴82(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php23screenpipe"Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges 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),done.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-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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-email-text-relay) $ [100% C4 8• Tue 19 May 15:59:08₴84-zsh*5 |-APP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58599
|
2078
|
2
|
2026-05-19T12:59:03.398320+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195543398_m1.jpg...
|
PhpStorm
|
faVsco.js – .env.production-eu
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1038026243914274786
|
-3772042409263646193
|
idle
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}...
|
58597
|
NULL
|
NULL
|
NULL
|
|
58598
|
2079
|
2
|
2026-05-19T12:58:40.666551+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195520666_m2.jpg...
|
PhpStorm
|
faVsco.js – .env.production-eu
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}...
|
[{"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":"14","depth":4,"bounds":{"left":0.390625,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40226063,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.24740623,"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.24740623,"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\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.0,"width":0.2662899,"height":1.0},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1038026243914274786
|
-3772042409263646193
|
idle
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}...
|
58596
|
NULL
|
NULL
|
NULL
|
|
58597
|
2078
|
1
|
2026-05-19T12:58:32.065030+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195512065_m1.jpg...
|
PhpStorm
|
faVsco.js – .env.production-eu
|
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:...
|
[{"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}]...
|
178754188049843600
|
-8708757530921030714
|
idle
|
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:
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp• 0APP (-zsh)APP (-zsh)DOCKER• ₴1DEV (docker)₴82(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php23screenpipe"Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges 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),done.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-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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-email-text-relay) $ [100% C4 8• Tue 19 May 15:58:310 ₴4-zsh*5|=APP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58596
|
2079
|
1
|
2026-05-19T12:58:07.435448+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195487435_m2.jpg...
|
PhpStorm
|
faVsco.js – .env.production-eu
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormFV faVsco.js~ProjectVIewINavicarecodeg9 JY PhostormFV faVsco.js~ProjectVIewINavicarecodeg9 JY-20915-add-domain-specific-email-text-reC RecipientServiceTest.php> O MailMeetingGenerator> D NotificationIm PecallAl> D Team> D Telephony> DUser@ userPilot> D Webhook© ActivityServiceTest.php© ApiResponseServiceTest.phpc) Instanumeeunoservice l est.oho° IntercomServiceTest.phpc) KioskServiceTest.ohoc) PlanhatServiceTest.phoC PlavbackVideoOnIvServiceTest.phpc) PlavbookCategorvServiceTest.ohocResolveTeamermconnectionirest.onoc) SackServiceTest.onoE SocialAccountServiceTest.phpcTeamDeactivatedServiceTest.ohr© TeamOwnerServiceTest.php© TeamServiceTest.php© UserServiceTest.phpM TraitsDUseCasesmlicerD UtilsD ValidationT EnuminArrayTrait.php© HelpersTest.phpE InitialFrontendStateTest.php© MockUnusedDependencies Trait.php> Dunit2022© ApiGuardLogin.php© TestCase.php>C vendorô .editorconfig= envE .env.circlecienv.circleci-niahtlv= .env.local= env miarate= env.nikilocall= env.otherE .env.productionE .env.qa= env aa= anv rant) ConvertLeadActivities.php© SaveActivity.phpo synerorlannatpnp© UserAutomatedkeporscontroller.phpxPlaybackController.phpE custom.logE laravel.log4 SF [jiminny@localhost]© TextRelayService.phpE .env.production-eu X A console [EU]FFPROBE_PATH=/usr/local/bin/ffprobeFFMPEG_PATH=/usr/local/bin/ffmpegA HS_Jocal [jiminny@localhost]A console (STAGING]A console [PROD]© AcuvityLeadConvertea.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) AutomatedReportsRepositorv.php(C) CreateSmsSentEvent.phg© AutomatedReportsCommand.phpC) PlanhatActivityListener.phpphp api_v2.php©AskAnythingPromptService.phpC) RequestGenerateReportJob.php(C)AutomatedReportResult.ohv(C) AutomatedReport.ohouse Jiminny|Services\Kiosk\AutomatedReports\ReportSortDirection;use Jiminny|Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,238239240A14 22 A Y241242class UserAutomatedReportsController extends Controllerpublic const int RESULTS_PER_PAGE = 25;public const string SORT_COLUMN = 'sort_column';public const strina SORT DIRECTION = 'sort direction':30 0,public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelprivate readonly ApiResponseService $apiResponseService,orivate readoni.v Resnonse Sresnonse..private readonly PlanhatService $planhatService,) 1..}40(@g›POST /api/v1/automated-reports/interest 1 usagepublic function trackInterest(Request $request): JsonResponse(..}* @throws ApplicationException58 (gg >GET /api/v1/automated-reportspublic function List(Request Srequest): JsonResponsef...,DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest, string $uvid): JsonResponset..., -269270271272L.2732741275276278SQS_AWS_ACCESS_KEY=_QUEUE_ACCESS_KEYSQS_AWS_SECRET_KEY=_QUEUE_SECRET_KEYSQS_PREFIX=_QUEUE_PREFIXSQS_QUEUE=_QUEUE_SQS_NAMESQS_FIFO_QUEUE=_QUEUE_SQS_FIFO_NAME.FIFOSQS_REGION=_QUEUE_REGIONKINESIS_AWS_REGION=eU-west-1INSIGHTS_EVENTS_AWS_KINESIS_STREAM_NAME=activity-event-insightsINSIGHTS_METRICS_AWS_KINESIS_STREAM_NAME=activity-metric-insightsGOOGLE_TEXT_RELAY_MAILBOX=catch-allGOOGLETEXT RELAY_MATLB0X=catch-al-euGOOGLE_TEXT_RELAY_HOST=txt.jiminny.comGOOGLE_TEXTRELAY_TOPTCEpronects/niminnv-web/topics/com.7iminnv.txtGOOGLE_TEXT_RELAY_SUBSCRIPTION=projects/jiminny-web/subscriptions/relayELASTICSEARCH_HOST=_AWS_ELASTIC_SEARCH_HOSTELASTICSEARCH PORTE80ELASTICSEARCH_TRANSPORT=HttpCDN_URL=[URL_WITH_CREDENTIALS] SWF-16* Reiect allAccent allWN Windsurf Toams 248-1 UTF.8f?4 spaces...
|
NULL
|
3589436215641918400
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormFV faVsco.js~ProjectVIewINavicarecodeg9 JY PhostormFV faVsco.js~ProjectVIewINavicarecodeg9 JY-20915-add-domain-specific-email-text-reC RecipientServiceTest.php> O MailMeetingGenerator> D NotificationIm PecallAl> D Team> D Telephony> DUser@ userPilot> D Webhook© ActivityServiceTest.php© ApiResponseServiceTest.phpc) Instanumeeunoservice l est.oho° IntercomServiceTest.phpc) KioskServiceTest.ohoc) PlanhatServiceTest.phoC PlavbackVideoOnIvServiceTest.phpc) PlavbookCategorvServiceTest.ohocResolveTeamermconnectionirest.onoc) SackServiceTest.onoE SocialAccountServiceTest.phpcTeamDeactivatedServiceTest.ohr© TeamOwnerServiceTest.php© TeamServiceTest.php© UserServiceTest.phpM TraitsDUseCasesmlicerD UtilsD ValidationT EnuminArrayTrait.php© HelpersTest.phpE InitialFrontendStateTest.php© MockUnusedDependencies Trait.php> Dunit2022© ApiGuardLogin.php© TestCase.php>C vendorô .editorconfig= envE .env.circlecienv.circleci-niahtlv= .env.local= env miarate= env.nikilocall= env.otherE .env.productionE .env.qa= env aa= anv rant) ConvertLeadActivities.php© SaveActivity.phpo synerorlannatpnp© UserAutomatedkeporscontroller.phpxPlaybackController.phpE custom.logE laravel.log4 SF [jiminny@localhost]© TextRelayService.phpE .env.production-eu X A console [EU]FFPROBE_PATH=/usr/local/bin/ffprobeFFMPEG_PATH=/usr/local/bin/ffmpegA HS_Jocal [jiminny@localhost]A console (STAGING]A console [PROD]© AcuvityLeadConvertea.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) AutomatedReportsRepositorv.php(C) CreateSmsSentEvent.phg© AutomatedReportsCommand.phpC) PlanhatActivityListener.phpphp api_v2.php©AskAnythingPromptService.phpC) RequestGenerateReportJob.php(C)AutomatedReportResult.ohv(C) AutomatedReport.ohouse Jiminny|Services\Kiosk\AutomatedReports\ReportSortDirection;use Jiminny|Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,238239240A14 22 A Y241242class UserAutomatedReportsController extends Controllerpublic const int RESULTS_PER_PAGE = 25;public const string SORT_COLUMN = 'sort_column';public const strina SORT DIRECTION = 'sort direction':30 0,public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelprivate readonly ApiResponseService $apiResponseService,orivate readoni.v Resnonse Sresnonse..private readonly PlanhatService $planhatService,) 1..}40(@g›POST /api/v1/automated-reports/interest 1 usagepublic function trackInterest(Request $request): JsonResponse(..}* @throws ApplicationException58 (gg >GET /api/v1/automated-reportspublic function List(Request Srequest): JsonResponsef...,DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest, string $uvid): JsonResponset..., -269270271272L.2732741275276278SQS_AWS_ACCESS_KEY=_QUEUE_ACCESS_KEYSQS_AWS_SECRET_KEY=_QUEUE_SECRET_KEYSQS_PREFIX=_QUEUE_PREFIXSQS_QUEUE=_QUEUE_SQS_NAMESQS_FIFO_QUEUE=_QUEUE_SQS_FIFO_NAME.FIFOSQS_REGION=_QUEUE_REGIONKINESIS_AWS_REGION=eU-west-1INSIGHTS_EVENTS_AWS_KINESIS_STREAM_NAME=activity-event-insightsINSIGHTS_METRICS_AWS_KINESIS_STREAM_NAME=activity-metric-insightsGOOGLE_TEXT_RELAY_MAILBOX=catch-allGOOGLETEXT RELAY_MATLB0X=catch-al-euGOOGLE_TEXT_RELAY_HOST=txt.jiminny.comGOOGLE_TEXTRELAY_TOPTCEpronects/niminnv-web/topics/com.7iminnv.txtGOOGLE_TEXT_RELAY_SUBSCRIPTION=projects/jiminny-web/subscriptions/relayELASTICSEARCH_HOST=_AWS_ELASTIC_SEARCH_HOSTELASTICSEARCH PORTE80ELASTICSEARCH_TRANSPORT=HttpCDN_URL=[URL_WITH_CREDENTIALS] SWF-16* Reiect allAccent allWN Windsurf Toams 248-1 UTF.8f?4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58593
|
NULL
|
0
|
2026-05-19T12:57:42.050369+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195462050_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-a PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reroledey© InternetMessagelnterface.php© MailChannelService.phpD MeetingGeneratoruNotricationWOAUtnZD RecallAW Telephonvl User?ilotWeohook© AbstractService.php© ActivityProviderFactory.php© ActivityService.php© ApiResponseService.php@ ConferenceService.php(c) InciahtSoatService nhn© InstantMeetingService.php© IntercomService.php© IpapiClient.php(@) InaniService.php© ParticipantShareService.php@ DlanhatService.onp© PlaybackService.php(© PlaybackVideoOnlyService.php© PlaybookCategoryService.php• PlaylistGeneratorInterface.phpc) Resolveleamermeonnection.phc(c) SimpleThrottleservice.phc(c) SlackService.phc© SocialAccountService.phpc) SortPhoneService.ohr@ TeamDeactivatedService.ohv(C) [EMAIL]@ TeamService.php(C) TranscodeParameterResolver.oho@ UserService.ohn(C) Uuid.nho> D Traits• MUseCases> MUserMUtils• M Validationi•MVophp helpers.php© InitialFrontendState.php(e) liminny nhn(e) Dian nhn© ConvertLeadActivities.php(C) SaveActivity.pnp© SyncPlanhat.phpo synerorlannatpnp© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php(C) AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;BI4KLAYuse Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository,private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lanilv1lautomated-renorts/interest 1usadekalusspublic function trackInterest(Request Srequest): JsonResponse{...}* Othnows ApplicationExceptionGET /api/v1/automated-reports58(09 >public function list(Request Srequest): JsonResponse(...}125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}=custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextRelayService.php X A console [EU]A console [STAGING]stohenamespace Jininny services Marl>use..•class Textrelayservicepublic function constructor...* Fetch the latest messages since the last sunc.public function sync): arraySmailbox = config( key: 'jiminny.google_text_user'):SexpectedRecipient = Smailbox .'@' . config( key:"jiminny.google_text_host'):AccentRejectScenvice = Sthis->ae+Senvice(Smailhoxa•$messageHistory = $this->getHistory(Sservice):$messageIds = 0]:foreach (SmessageHistory as Shistories) {Smocconos = Chictonioc-moccanocAddod.foreach (Smessages as Smessage)smessageld = smessaqe->messaqe->10*if (!Sthis->isForCurrentEnvironment($service, $mailbox, Smessageld. SexpectedRecipient)) {SrelavedText = TextRelay:: where, colun'email provider id', Smessageld)->firsto:if (Srelavedtext === nulbSrelavedText = TextRelav::creatediTextRelav: : PROVIDER GSUMTEI=> TextRelav•• STATUIS pROcesStNG..Sjob = (new EmailTextRelay(SmessageId, SrelayedText))->onQueue( queue: Constants: :QUEUE_EMAILS),dicnatchl Caoh)"SmocconoTdell - SmoccanoTd.Accept File &~ X Reject File 4 * €kal100% 5• Tue 19 May 15:57:41AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Duplicati+0 ..yes implement ahnd exaplin the whole flow. What triggers the taxt vis email. webhooks and everythingThoughts6/ 10 tasks donec Create лKA licket with tille and descrlpuion• Change approach to email-based filtering instead of domain-basedThoughtsRead TextRelayService.phpTextRelayService.ohg" TeytRelavService.nhvU .env.production-eu133 tokens* Reiect allAccent alliAsk anvthina 884-L)+ @Code SWF-16WN Windsurf Toams 28-1UTF.8io 4 spaces...
|
NULL
|
-4745947221559732604
|
NULL
|
idle
|
ocr
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-a PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reroledey© InternetMessagelnterface.php© MailChannelService.phpD MeetingGeneratoruNotricationWOAUtnZD RecallAW Telephonvl User?ilotWeohook© AbstractService.php© ActivityProviderFactory.php© ActivityService.php© ApiResponseService.php@ ConferenceService.php(c) InciahtSoatService nhn© InstantMeetingService.php© IntercomService.php© IpapiClient.php(@) InaniService.php© ParticipantShareService.php@ DlanhatService.onp© PlaybackService.php(© PlaybackVideoOnlyService.php© PlaybookCategoryService.php• PlaylistGeneratorInterface.phpc) Resolveleamermeonnection.phc(c) SimpleThrottleservice.phc(c) SlackService.phc© SocialAccountService.phpc) SortPhoneService.ohr@ TeamDeactivatedService.ohv(C) [EMAIL]@ TeamService.php(C) TranscodeParameterResolver.oho@ UserService.ohn(C) Uuid.nho> D Traits• MUseCases> MUserMUtils• M Validationi•MVophp helpers.php© InitialFrontendState.php(e) liminny nhn(e) Dian nhn© ConvertLeadActivities.php(C) SaveActivity.pnp© SyncPlanhat.phpo synerorlannatpnp© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.php)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php(C) AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;BI4KLAYuse Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository,private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lanilv1lautomated-renorts/interest 1usadekalusspublic function trackInterest(Request Srequest): JsonResponse{...}* Othnows ApplicationExceptionGET /api/v1/automated-reports58(09 >public function list(Request Srequest): JsonResponse(...}125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}=custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextRelayService.php X A console [EU]A console [STAGING]stohenamespace Jininny services Marl>use..•class Textrelayservicepublic function constructor...* Fetch the latest messages since the last sunc.public function sync): arraySmailbox = config( key: 'jiminny.google_text_user'):SexpectedRecipient = Smailbox .'@' . config( key:"jiminny.google_text_host'):AccentRejectScenvice = Sthis->ae+Senvice(Smailhoxa•$messageHistory = $this->getHistory(Sservice):$messageIds = 0]:foreach (SmessageHistory as Shistories) {Smocconos = Chictonioc-moccanocAddod.foreach (Smessages as Smessage)smessageld = smessaqe->messaqe->10*if (!Sthis->isForCurrentEnvironment($service, $mailbox, Smessageld. SexpectedRecipient)) {SrelavedText = TextRelay:: where, colun'email provider id', Smessageld)->firsto:if (Srelavedtext === nulbSrelavedText = TextRelav::creatediTextRelav: : PROVIDER GSUMTEI=> TextRelav•• STATUIS pROcesStNG..Sjob = (new EmailTextRelay(SmessageId, SrelayedText))->onQueue( queue: Constants: :QUEUE_EMAILS),dicnatchl Caoh)"SmocconoTdell - SmoccanoTd.Accept File &~ X Reject File 4 * €kal100% 5• Tue 19 May 15:57:41AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Duplicati+0 ..yes implement ahnd exaplin the whole flow. What triggers the taxt vis email. webhooks and everythingThoughts6/ 10 tasks donec Create лKA licket with tille and descrlpuion• Change approach to email-based filtering instead of domain-basedThoughtsRead TextRelayService.phpTextRelayService.ohg" TeytRelavService.nhvU .env.production-eu133 tokens* Reiect allAccent alliAsk anvthina 884-L)+ @Code SWF-16WN Windsurf Toams 28-1UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58592
|
NULL
|
0
|
2026-05-19T12:57:26.935140+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195446935_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• 0ShellEditViewSessionScriptsProfilesWindow iTerm2• 0ShellEditViewSessionScriptsProfilesWindowHelp100% C4 8• Tue 19 May 15:57:26APP (-zsh)APP (-zsh)DOCKER• ₴1DEV (docker)₴82(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php23screenpipe"Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges 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),done.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-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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-email-text-relay) $ [0 ₴4-zsh*5 |-APP...
|
NULL
|
4575141839763889315
|
NULL
|
idle
|
ocr
|
NULL
|
iTerm2• 0ShellEditViewSessionScriptsProfilesWindow iTerm2• 0ShellEditViewSessionScriptsProfilesWindowHelp100% C4 8• Tue 19 May 15:57:26APP (-zsh)APP (-zsh)DOCKER• ₴1DEV (docker)₴82(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php23screenpipe"Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges 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),done.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-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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-email-text-relay) $ [0 ₴4-zsh*5 |-APP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58591
|
2077
|
17
|
2026-05-19T12:57:07.079777+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195427079_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-a PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reroledey© ConvertLeadActivities.php(C) SaveActivity.pnp© SyncPlanhat.phpo [EMAIL]© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.phpphp.phpstorm.meta.php)Inteqrationapp/service.ono) LeadConverted.pho=.phpunit.result.cacheE .prettierignoreC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php= windsurtrules(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.phpphpde neloer.onophpide_ heloer_models.oho© AutomatedReportResult.php(C) AutomatedReport.ohophp artisanM'CLAUDE.mduse Jiminny \Services \Kiosk \AutomatedReports \ReportSortDirection;BI4NLAYuse Jiminny \Services\PlanhatService;comooser.isonuse ectumtnace hutp kequestcomooser.lockuse Inrowable,dependency-checker.json8 dev isonclass useraucomacedreporesconcrocler excenas controuten= ids tytl=infection.ison.distMLINSTAlI.mdpublic const int RESULTS PER_ PAGE = 25:MLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllpublic const string SORT COLUMN = 'sort column':M Makefile1рackace-lock. sonE phpstan.neon.distpublic const strina SORT DIRECTION = "sort direction':= phostan-baseline.neon<> phpunit.xmlpublic function constructTe raw sal query.saprivate readonly AutomatedReportsRepository $automatedReportsRepository.M+ README.mdsô sonar-proiect propertiesE test.ovprivate readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.‹> Untitled Diaaram.xmliprivate readonly PlanhatService $planhatServiceusvetur.confio.is1...M+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and ConsolespoST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request Srequest): JsonResponset...}v batabase Consolesv AEU# console (EUl* Othnows ApplicationExceptionI DEAL RISKS (SU1A DifeulGET /api/v1/automated-reportsA EU (EU]58(09 >public function list(Request Srequest): JsonResponse(...}v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostl125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest, string $uuid): JsonResponse...}lA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD1LDIIPROD=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextRelayService.php X A console [EU]A console [STAGING]class TextRelayServicepublic function constructOf...}* recch che lacesc messages since che lasc sync.pubLic tunction syncu: array$mailbox = config('Jiminny google_text_user"):$expectedRecipient = $mailbox. '@'. config('jiminny.google_text_host');AccentReñectSservice = Sthis->qetServiceSmartbox)SmessageHistorv = Sthis->aetHistorv(Sservice)sSmessagelds = [1:foreach (SmessageHistory as Shistories) 1Smessages = Shistories->messagesAdded:Fanpach Smeccanps as Smeccane4Smeccaaeld = Smeccane->meccane->id•if (!$this->isForCurrentEnvironment($service, $mailbox, Smessageld, $expectedRecipient)) {continue:SrelayedText = TextRelay::where('email_provider_id', Smessageld)->firstO:if (SrelayedText === null) {SrelavedText = TextRelay::create(lemall provider= TextRelay: : PROVIDER_GSUITE)'emarl provider_ 1d => Smessageid"status' => Textrelav::STATUS_PROCESSING..Siob = (new EmailTextRelav(Smessageld. SrelavedText))->onQueue(Constants::OUEUE EMATLS),disnatch(Siob)$messageIds[] = $messageIdAccept File &~ X Reject File 4%€kal100% 5• Tue 19 May 15:57:06AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascade• Text Relay Duplicati+0 ..yes implement ahnd exaplin the whole flow. What triggers the taxt vis email. webhooks and everythingThoughts6/ 10 tasks donec Create лKA licket with tille and descrlpuionChange approach lo emall-based tiltering instead or domain-basedThoughtsRead TextRelayService.phpTextRelayService.ohgAsk anvthing 884-D)+ @Code SWF-16* Reiect allAccent alliWN Windsurf Toams 28-1UTF.8Po 4 spaces...
|
NULL
|
1942497150679574716
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-a PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reroledey© ConvertLeadActivities.php(C) SaveActivity.pnp© SyncPlanhat.phpo [EMAIL]© AcuvityLeadConvertea.php© UserAutomatedReportsController.php x © PlaybackController.phpphp.phpstorm.meta.php)Inteqrationapp/service.ono) LeadConverted.pho=.phpunit.result.cacheE .prettierignoreC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php= windsurtrules(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.phpphpde neloer.onophpide_ heloer_models.oho© AutomatedReportResult.php(C) AutomatedReport.ohophp artisanM'CLAUDE.mduse Jiminny \Services \Kiosk \AutomatedReports \ReportSortDirection;BI4NLAYuse Jiminny \Services\PlanhatService;comooser.isonuse ectumtnace hutp kequestcomooser.lockuse Inrowable,dependency-checker.json8 dev isonclass useraucomacedreporesconcrocler excenas controuten= ids tytl=infection.ison.distMLINSTAlI.mdpublic const int RESULTS PER_ PAGE = 25:MLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllpublic const string SORT COLUMN = 'sort column':M Makefile1рackace-lock. sonE phpstan.neon.distpublic const strina SORT DIRECTION = "sort direction':= phostan-baseline.neon<> phpunit.xmlpublic function constructTe raw sal query.saprivate readonly AutomatedReportsRepository $automatedReportsRepository.M+ README.mdsô sonar-proiect propertiesE test.ovprivate readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.‹> Untitled Diaaram.xmliprivate readonly PlanhatService $planhatServiceusvetur.confio.is1...M+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and ConsolespoST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request Srequest): JsonResponset...}v batabase Consolesv AEU# console (EUl* Othnows ApplicationExceptionI DEAL RISKS (SU1A DifeulGET /api/v1/automated-reportsA EU (EU]58(09 >public function list(Request Srequest): JsonResponse(...}v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostl125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest, string $uuid): JsonResponse...}lA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD1LDIIPROD=custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]© TextRelayService.php X A console [EU]A console [STAGING]class TextRelayServicepublic function constructOf...}* recch che lacesc messages since che lasc sync.pubLic tunction syncu: array$mailbox = config('Jiminny google_text_user"):$expectedRecipient = $mailbox. '@'. config('jiminny.google_text_host');AccentReñectSservice = Sthis->qetServiceSmartbox)SmessageHistorv = Sthis->aetHistorv(Sservice)sSmessagelds = [1:foreach (SmessageHistory as Shistories) 1Smessages = Shistories->messagesAdded:Fanpach Smeccanps as Smeccane4Smeccaaeld = Smeccane->meccane->id•if (!$this->isForCurrentEnvironment($service, $mailbox, Smessageld, $expectedRecipient)) {continue:SrelayedText = TextRelay::where('email_provider_id', Smessageld)->firstO:if (SrelayedText === null) {SrelavedText = TextRelay::create(lemall provider= TextRelay: : PROVIDER_GSUITE)'emarl provider_ 1d => Smessageid"status' => Textrelav::STATUS_PROCESSING..Siob = (new EmailTextRelav(Smessageld. SrelavedText))->onQueue(Constants::OUEUE EMATLS),disnatch(Siob)$messageIds[] = $messageIdAccept File &~ X Reject File 4%€kal100% 5• Tue 19 May 15:57:06AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascade• Text Relay Duplicati+0 ..yes implement ahnd exaplin the whole flow. What triggers the taxt vis email. webhooks and everythingThoughts6/ 10 tasks donec Create лKA licket with tille and descrlpuionChange approach lo emall-based tiltering instead or domain-basedThoughtsRead TextRelayService.phpTextRelayService.ohgAsk anvthing 884-D)+ @Code SWF-16* Reiect allAccent alliWN Windsurf Toams 28-1UTF.8Po 4 spaces...
|
58590
|
NULL
|
NULL
|
NULL
|
|
58589
|
2076
|
20
|
2026-05-19T12:56:53.950317+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195413950_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹ $0labl• •APP (-zsh)DOCKER• ₴1DEV (docker)₴82APP (-zsh)(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php23ffmpegUntracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges 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),done.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-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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-email-text-relay) $ [100% C8• Tue 19 May 15:56:530 ₴4-zsh85APP...
|
NULL
|
3217892454072412326
|
NULL
|
idle
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹ $0labl• •APP (-zsh)DOCKER• ₴1DEV (docker)₴82APP (-zsh)(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php23ffmpegUntracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges 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),done.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-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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-email-text-relay) $ [100% C8• Tue 19 May 15:56:530 ₴4-zsh85APP...
|
58587
|
NULL
|
NULL
|
NULL
|
|
58588
|
2077
|
15
|
2026-05-19T12:56:28.197033+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195388197_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
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
|
idle
|
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
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reroledey© SyncPlanhat.php© [EMAIL]© AcuvityLeadConvertea.php© UserAutomatedReportsController.php xPlaybackController.phrphp.phpstorm.meta.php=.phpunit.result.cacheE .prettierignore= windsurtrules)Inteqrationapp/service.ono) LeadConverted.phoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.phpphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.md© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk \AutomatedReports\ReportSortDirection;use Jiminny \Services\PlanhatService;comooser.isonuse ectumtnace hutp kequestcomooser.lockuse Inrowable,dependency-checker.json8 dev isonclass userauconacedreporesconcrocler excenas controute= ids tytl=infection.ison.distMLINSTAlI.mdpublic const int RESULTS PER_ PAGE = 25:MLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllpublic const string SORT COLUMN = 'sort column':M Makefile1рackace-lock. sonE phpstan.neon.distpublic const strina SORT DIRECTION = "sort direction':= phostan-baseline.neon<> phpunit.xmlpublic function constructTe raw sal query.saprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelM+ README.mdsô sonar-proiect propertiesE test.ovorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.‹> Untitled Diaaram.xmliprivate readonly PlanhatService $planhatServiceusvetur.confio.is1...M+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and ConsolespoST lani/v1lautomated-renorts/interest 1usaaepublic function trackInterest(Request $request): JsonResponse(...}v batabase Consolesv AEU# console (EUl* Othnows ApplicationExceptionI DEAL RISKS (SU1A DITEUlGET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}A EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD158(09 >125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest, string Suuid): JsonResponsef...}LDIPROD577E| A14 X2 A Y ABBaS610611613615618=laravel.logA SF [jiminny@localhost]4 HS_local [jiminny@localhost]console [PRODI X# console [euyCONCAT(U.1d, CASE WHEN U.1d = t.owner_id THEN " (owner)' ELSE"'END) AS user_I=141 41440 y.65 A Vsa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociable 1diJOIN teams t 1.n<->1: on t.id = U.team_idWHERE u.team id = 581 and sa.provider = 'salesfonce';SELECT * FROM automated_report_results order by id desc;select * from featureslcolont * fnom toam foatunoc whono foatuno id = 10.select * from teams where id = 556:select * from automated_reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044["pdf" "podcast"]SELECT * FROM automated_ report_results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:select * from automated_report_results order by id desc;SELECT * FROM automated_ report_resultsWHERE id = 1919:select * from automated report results WHERE report id = 54:select * from opportunities where id = 7594349:SELECT * FROM teams WHERE name LIKE 1%Les%': # 711. 692. 16067-jiminnvintegration0lesmills.comselect * From mlavbooks where team 1d = 111. # event 226147SEE * SROM navhook catedories WHERE DLavbook 10 = 55151SELECT * FROM eom fields WHERE eom confiauration 1d = 692 and obnect tvne = 'eventi.WHEREI1d = 2261471crm_field_values WHERE crm_field_id = 226147;SELECT * FROMIerm confiaurations WHEREid = 692;111111CONCAT(u.id, CASE WHEN v.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,uemai]sa.*t.owner id FROM social accounts saJOIN users u on u.id = sa.sociable idIOTM toome + 1 necsi: on + 4d e m toom %dWHERE u.team id = 711 and sa.provider = 'salesforce' :SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leadsselect * from calendarsceicortid AS team idhal100% 5• Tue 19 May 15:56:28AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascade• Text Relay Duplicati+0 ..yes implement ahnd exaplin the whole flow. What triggers the taxt vis email. webhooks and everythingAsk anvthina (%AL)"Codo CWE.1GW Windsurf Teams 594:60UTE.9Aensod...
|
58586
|
NULL
|
NULL
|
NULL
|
|
58587
|
2076
|
19
|
2026-05-19T12:56:21.331264+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195381331_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• 0ShellEditViewSessionScriptsProfilesWindow iTerm2• 0ShellEditViewSessionScriptsProfilesWindowHelpAPP (-zsh)APP (-zsh)DOCKER• ₴1DEV (docker)₴82(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php23screenpipe"Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges 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),done.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-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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-email-text-relay) $ [0 ₴4100% C4 8• Tue 19 May 15:56:20-zsh181*5|=APP...
|
NULL
|
7795907722476521851
|
NULL
|
app_switch
|
ocr
|
NULL
|
iTerm2• 0ShellEditViewSessionScriptsProfilesWindow iTerm2• 0ShellEditViewSessionScriptsProfilesWindowHelpAPP (-zsh)APP (-zsh)DOCKER• ₴1DEV (docker)₴82(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php23screenpipe"Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges 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),done.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-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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-email-text-relay) $ [0 ₴4100% C4 8• Tue 19 May 15:56:20-zsh181*5|=APP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58586
|
2077
|
14
|
2026-05-19T12:55:54.601564+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195354601_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
PhostormVIewINavicarecode Project: faVsco.js, menu
PhostormVIewINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reroledey© SyncPlanhat.php© [EMAIL]© AcuvityLeadConvertea.php© UserAutomatedReportsController.php xPlaybackController.phrphp.phpstorm.meta.php=.phpunit.result.cacheE .prettierignore= windsurtrules)Inteqrationapp/service.ono) LeadConverted.phoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.phpphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.md© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk \AutomatedReports \ReportSortDirection;use Jiminny \Services\PlanhatService;comooser.isonuse ectumtnace hutp kequestcomooser.lockuse Inrowable,dependency-checker.json8 dev isonclass userauconacedreporesconcrocler excenas controute= ids tytl=infection.ison.distMLINSTAlI.mdpublic const int RESULTS PER_ PAGE = 25:MLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllpublic const string SORT COLUMN = 'sort column':M Makefile1рackace-lock. sonE phpstan.neon.distpublic const strina SORT DIRECTION = "sort direction':= phostan-baseline.neon<> phpunit.xmlpublic function construct.Te raw sal query.saprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelM+ README.mdsô sonar-proiect propertiesE test.ovorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.‹> Untitled Diaaram.xmliprivate readonly PlanhatService $planhatServiceusvetur.confio.is1...M+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and ConsolespoST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request Srequest): JsonResponsef...}v batabase Consolesv AEU# console (EUl* Othnows ApplicationExceptionI DEAL RISKS (SU1A DifeulGET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}A EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD158(09 >125 0)DELETE/api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}LDIPROD577E| A14 X2 A Y $883610611613615618=laravel.logA SF [jiminny@localhost]4 HS_local [jiminny@localhost]« console [PROD] X# console [euyA console [STAGING]CONCAT(U.Id, CASE WHEN U.1d = t.owner_Id THEN " (owner)" ELSE" END) AS USer_I141 41440 y.65 A Vsa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociable 1diJOIN teams t 1.n<->1: on t.id = U.team_idWHERE U.team_id = 581 and sa.provider = 'salesforce';SELECT * FROM automated_report_results order by id desc;select * from features;select * from team_features where feature_id = 40;select * from teams where id = 556:select * from automated_reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044["pdf" "podcast"]SELECT * FROM automated_ report_results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:select * from automated_report_results order by id desc;SELECT * FROM automated_ report_resultsWHERE id = 1919:select * from automated report results WHERE report id = 54:select * from opportunities where id = 7594349:SELECT * FROM teams WHERE name LIKE '%Les%': # 711, 692. [EMAIL] * From mlavbooks where team 1d = 111. # event 226147SELEd * FROM Dlavbook catedories WHERE DlLavbook 1d = 55151crm_field_values WHERE crm_field_id = 226147;SELECT * FROMIerm confiaurations WHERE 1d = 692CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "• END) AS user_id,uemai]sa.*t.owner id FROM social accounts saJOIN users u on u.id = sa.sociable idIOTM toome + 1 necsi: on + 4d e m toom idWUEDE „ toam id = 711 and sa.orovider = 'salesforce':SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leadsselect * from calendarsceicortid AS team idmibl100% 5• Tue 19 May 15:55:54AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Duplication+0 ..can i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a questionTnoughtror isYes, you can keep both email addresses in the same domain:current setup:• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)Proposed setupcatch-allatxt.iiminnv.com - used by US onlv• [EMAIL] - used by EU onlyYou would1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domainRegarding using jiminny.com domainYes, you could use the main domain instead of the txt.iminny.com subdomain:catch-allajiminny.com for UScatch-all-euaiiminnv.com for EUhowever, this would require• Changing the existing txt.jiminny.com setup (which is currently working for US)Micht artect existind users who have SMS nottications with @txt.liminnv.com reolv-to addresses• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comnnv.com. This is the least disruptivechange - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based tilterina aporoach usina [EMAIL]?1 ***ves imolement ahndel"Codo CWE.1GNN Windeurf Toame K0Л•60UTE.9Aensod...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58584
|
2077
|
13
|
2026-05-19T12:55:50.141623+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195350141_m2.jpg...
|
PhpStorm
|
APP (-zsh)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PnostorimFV faVsco.js°9 [EMAIL]=.phpunit.result.ca PnostorimFV faVsco.js°9 [EMAIL]=.phpunit.result.cacheE .prettierignore= windsurtrulesphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.mdcomooser.isoncomooser.lockdependency-checker.json8 dev ison= ids tytl=infection.ison.distMLINSTAlI.mdMLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllM Makefilepackace-lock. sonE phpstan.neon.dist= phostan-baseline.neon<> phpunit.xmlTe raw sal query.saM+ README.mdsô sonar-proiect propertiesE test.ov‹> Untitled Diaaram.xmliusvetur.confio.isM+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and Consolesv batabase Consolesv AEU# console (EUlI DEAL RISKS (SU1A DITEUlA EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD1LDIIPRODC ActivityController.ong© SyncPlanhat.php© SyncToPlanhat.phpAcuivityLeadConverted.php© UserAutomatedReportsController.php xPlaybackController.phr)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk \AutomatedReports\ReportSortDirection;use Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTTON = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService,1...poST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request $request): JsonResponse(...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest, string Suuid): JsonResponsef...}577E| A14 X2 A Y ABBaS610611613615618=laravel.logA SF [jiminny@localhost]4 HS_local [jiminny@localhost]« console [PROD] X# console [euyA console [STAGING]CONCAT(U.Id, CASE WHEN U.1d = t.owner_Id THEN " (owner)" ELSE" END) AS USer_I141 41 440 V65 A Vsa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociable 1diJOIN teams t 1.n<->1: on t.id = u.team_idWHERE U.team_id = 581 and sa.provider = 'salesforce';SELECT * FROM automated_report_results order by id desc;select * from features;select * from team_features where feature_id = 40;select * from teams where id = 556:select * from automated_reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044["pdf" "podcast"]SELECT * FROM automated_ report_results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:select * from automated_report_results order by id desc;SELECT * FROM automated_ report_resultsWHERE id = 1919:select * from automated report results WHERE report id = 54:select * from opportunities where id = 7594349:SELECT * FROM teams WHERE name LIKE '%Les%': # 711, 692. [EMAIL] * From mlavbooks where team 1d = 111. # event 226147SELEd * FROM Dlavbook catedories WHERE DlLavbook 1d = 55151crm_field_values WHERE crm_field_id = 226147;SELECT * FROMIerm confiaurations WHERE 1d = 692CONCAT(u.id, CASE WHEN v.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,uemai]sa.*t.owner id FROM social accounts saJOIN users u on u.id = sa.sociable idIOTM toome + 1 necsi: on + 4d e m toom %dWHERE u.team id = 711 and sa.provider = 'salesforce' :SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leadsselect * from calendarsceicortid AS team idmibl100% 5• lue 1y мay 10:00.4%AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Duplication+O •can i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a questionTnoughtror isYes, you can keep both email addresses in the same domain:current setup:• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)Proposed setup• [EMAIL] - used by US only• [EMAIL] - used by EU onlyYou would1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domainRegarding using jiminny.com domainYes, you could use the main domain instead of the txt.iminny.com subdomain:catch-allajiminny.com for UScatch-all-euaiiminnv.com for EUhowever, this would require• Changing the existing txt.jiminny.com setup (which is currently working for US)Micht artect existind users who have SMS nottications with @txt.liminnv.com reolv-to addresses• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comnnv.com. This is the least disruptivechange - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based tilterina aporoach usina [EMAIL]?1 ***ves imolement"Codo CWE.1GNN Windeurf Toame 504.60UTE.9...
|
NULL
|
2651846070380758737
|
NULL
|
app_switch
|
ocr
|
NULL
|
PnostorimFV faVsco.js°9 [EMAIL]=.phpunit.result.ca PnostorimFV faVsco.js°9 [EMAIL]=.phpunit.result.cacheE .prettierignore= windsurtrulesphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.mdcomooser.isoncomooser.lockdependency-checker.json8 dev ison= ids tytl=infection.ison.distMLINSTAlI.mdMLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllM Makefilepackace-lock. sonE phpstan.neon.dist= phostan-baseline.neon<> phpunit.xmlTe raw sal query.saM+ README.mdsô sonar-proiect propertiesE test.ov‹> Untitled Diaaram.xmliusvetur.confio.isM+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and Consolesv batabase Consolesv AEU# console (EUlI DEAL RISKS (SU1A DITEUlA EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD1LDIIPRODC ActivityController.ong© SyncPlanhat.php© SyncToPlanhat.phpAcuivityLeadConverted.php© UserAutomatedReportsController.php xPlaybackController.phr)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk \AutomatedReports\ReportSortDirection;use Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,class UserAutomatedReportsController extends Controllenpublic const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTTON = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService,1...poST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request $request): JsonResponse(...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest, string Suuid): JsonResponsef...}577E| A14 X2 A Y ABBaS610611613615618=laravel.logA SF [jiminny@localhost]4 HS_local [jiminny@localhost]« console [PROD] X# console [euyA console [STAGING]CONCAT(U.Id, CASE WHEN U.1d = t.owner_Id THEN " (owner)" ELSE" END) AS USer_I141 41 440 V65 A Vsa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociable 1diJOIN teams t 1.n<->1: on t.id = u.team_idWHERE U.team_id = 581 and sa.provider = 'salesforce';SELECT * FROM automated_report_results order by id desc;select * from features;select * from team_features where feature_id = 40;select * from teams where id = 556:select * from automated_reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044["pdf" "podcast"]SELECT * FROM automated_ report_results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:select * from automated_report_results order by id desc;SELECT * FROM automated_ report_resultsWHERE id = 1919:select * from automated report results WHERE report id = 54:select * from opportunities where id = 7594349:SELECT * FROM teams WHERE name LIKE '%Les%': # 711, 692. [EMAIL] * From mlavbooks where team 1d = 111. # event 226147SELEd * FROM Dlavbook catedories WHERE DlLavbook 1d = 55151crm_field_values WHERE crm_field_id = 226147;SELECT * FROMIerm confiaurations WHERE 1d = 692CONCAT(u.id, CASE WHEN v.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,uemai]sa.*t.owner id FROM social accounts saJOIN users u on u.id = sa.sociable idIOTM toome + 1 necsi: on + 4d e m toom %dWHERE u.team id = 711 and sa.provider = 'salesforce' :SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leadsselect * from calendarsceicortid AS team idmibl100% 5• lue 1y мay 10:00.4%AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Duplication+O •can i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a questionTnoughtror isYes, you can keep both email addresses in the same domain:current setup:• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)Proposed setup• [EMAIL] - used by US only• [EMAIL] - used by EU onlyYou would1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domainRegarding using jiminny.com domainYes, you could use the main domain instead of the txt.iminny.com subdomain:catch-allajiminny.com for UScatch-all-euaiiminnv.com for EUhowever, this would require• Changing the existing txt.jiminny.com setup (which is currently working for US)Micht artect existind users who have SMS nottications with @txt.liminnv.com reolv-to addresses• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comnnv.com. This is the least disruptivechange - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based tilterina aporoach usina [EMAIL]?1 ***ves imolement"Codo CWE.1GNN Windeurf Toame 504.60UTE.9...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58583
|
2076
|
17
|
2026-05-19T12:55:46.964749+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195346964_m1.jpg...
|
PhpStorm
|
APP (-zsh)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2•00ShellEditViewSessionScriptsProfilesWindow iTerm2•00ShellEditViewSessionScriptsProfilesWindowHelpCD0 l oAPP (-zsh)DOCKER• ₴1DEV (docker)₴82APP (-zsh)(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php*3screenpipe"Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpno changes 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),done.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.180), ecs eused o (from o)From github.com:jiminny/app16fa95720c..045ab2e1748a03ca9e03..84e7b10765JY-20842-remove-elastic-search-delete-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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 }Jext-relay) $|A0 ₴4100% C8• Tue 19 May 15:55:46T81-zsh*5APP...
|
NULL
|
-3598664635355967033
|
NULL
|
app_switch
|
ocr
|
NULL
|
iTerm2•00ShellEditViewSessionScriptsProfilesWindow iTerm2•00ShellEditViewSessionScriptsProfilesWindowHelpCD0 l oAPP (-zsh)DOCKER• ₴1DEV (docker)₴82APP (-zsh)(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.php*3screenpipe"Untracked 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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpno changes 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),done.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.180), ecs eused o (from o)From github.com:jiminny/app16fa95720c..045ab2e1748a03ca9e03..84e7b10765JY-20842-remove-elastic-search-delete-documents-> origin/JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-events-> origin/JY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dFast-forwardapp/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.json4842082front-end/yarn.lockroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php42862910 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-domain-specific-email-text-relaySwitched 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 }Jext-relay) $|A0 ₴4100% C8• Tue 19 May 15:55:46T81-zsh*5APP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58567
|
2076
|
7
|
2026-05-19T12:54:28.272288+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195268272_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
41
1
40
65
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
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 = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
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 = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
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 = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
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 = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
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 = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
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 = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
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 = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
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 = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
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 = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
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 = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE 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;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
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 = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
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 = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
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 = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
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 = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
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 = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * 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");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
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 = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
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 = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
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 = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -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":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>17 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","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":"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}]...
|
-2379275710453830218
|
2218652917440067141
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
41
1
40
65
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
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 = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
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 = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
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 = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
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 = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
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 = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
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 = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
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 = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
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 = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
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 = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
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 = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE 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;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
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 = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
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 = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
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 = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
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 = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
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 = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * 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");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
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 = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
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 = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
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 = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -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
|
|
58566
|
2076
|
6
|
2026-05-19T12:54:25.630110+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195265630_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
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
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
41
1
40
65
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
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 = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
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 = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
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 = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
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 = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
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 = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
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 = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
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 = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
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 = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
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 = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
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 = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE 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;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
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 = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
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 = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
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 = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
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 = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
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 = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * 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");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
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 = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
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 = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
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 = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -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;...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>17 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\UserAutomatedReports;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Http\\JsonResponse;\nuse Jiminny\\Exceptions\\ApplicationException;\nuse Jiminny\\Http\\Controllers\\Controller;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\ApiResponseService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\nuse Jiminny\\Services\\PlanhatService;\nuse Illuminate\\Http\\Request;\nuse Throwable;\n\nclass UserAutomatedReportsController extends Controller\n{\n public const int RESULTS_PER_PAGE = 25;\n\n public const string SORT_COLUMN = 'sort_column';\n\n public const string SORT_DIRECTION = 'sort_direction';\n\n public function __construct(\n private readonly AutomatedReportsRepository $automatedReportsRepository,\n private readonly AutomatedReportsService $automatedReportsService,\n private readonly ApiResponseService $apiResponseService,\n private readonly Response $response,\n private readonly PlanhatService $planhatService,\n ) {\n parent::__construct();\n }\n\n public function trackInterest(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n defer(\n fn () => $this->planhatService->track(\n user: $user,\n event: 'automated-reports-track-interest',\n )\n )->always();\n\n return $this->response->withOk();\n }\n\n /**\n * @throws ApplicationException\n */\n public function list(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $teamIds = $request->has('team')\n ? (array) $request->get('team')\n : [];\n $reportTypes = $request->has('report_type')\n ? (array) $request->get('report_type')\n : [];\n $name = $request->has('name') ? trim($request->get('name', '')) : null;\n\n try {\n $fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;\n $toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;\n } catch (\\Exception) {\n return $this->response->errorWrongArgs('Invalid date.');\n }\n\n $page = $request->has('page') ? (int) $request->get('page') : 1;\n $sort = ReportSort::tryFrom(\n $request->get(self::SORT_COLUMN, '')\n ) ?? ReportSort::GENERATED_AT;\n $sortDirection = ReportSortDirection::tryFrom(\n strtolower($request->get(self::SORT_DIRECTION, ''))\n ) ?? ReportSortDirection::DESC;\n\n $paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(\n user: $user,\n sort: $sort,\n sortDirection: $sortDirection,\n resultsPerPage: self::RESULTS_PER_PAGE,\n page: $page,\n fromDate: $fromDate,\n toDate: $toDate,\n teamIds: array_map('intval', $teamIds),\n reportTypes: $reportTypes,\n name: $name,\n );\n\n $reportResults = $this->automatedReportsService->transformReportResults(\n $paginatedUserReports->getCollection()\n );\n $team = $user->getTeam();\n $reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(\n shortVersion: true,\n team: $team\n );\n\n $data = $this->apiResponseService->fromPaginatorToArray(\n paginator: $paginatedUserReports,\n data: $reportResults,\n moreMeta: [\n self::SORT_COLUMN => $sort->value,\n self::SORT_DIRECTION => $sortDirection->value,\n ],\n filters: [\n $reportTypeFilter['id'] => $reportTypeFilter,\n ],\n );\n\n return $this->response->withArray($data);\n }\n\n public function delete(Request $request, string $uuid): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n try {\n $result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);\n\n if ($result === null) {\n return new JsonResponse(\n data: ['error' => 'Report not found'],\n status: JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $result->delete();\n\n return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);\n } catch (Throwable $e) {\n return new JsonResponse(\n data: ['error' => 'Failed to delete report result'],\n status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR\n );\n }\n }\n}","role_description":"text entry area","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":"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}]...
|
1527157909059370054
|
2218635325254022725
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\UserAutomatedReports;
use Illuminate\Support\Carbon;
use Illuminate\Http\JsonResponse;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Http\Controllers\Controller;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Models\User;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\ApiResponseService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
use Jiminny\Services\PlanhatService;
use Illuminate\Http\Request;
use Throwable;
class UserAutomatedReportsController extends Controller
{
public const int RESULTS_PER_PAGE = 25;
public const string SORT_COLUMN = 'sort_column';
public const string SORT_DIRECTION = 'sort_direction';
public function __construct(
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly ApiResponseService $apiResponseService,
private readonly Response $response,
private readonly PlanhatService $planhatService,
) {
parent::__construct();
}
public function trackInterest(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
defer(
fn () => $this->planhatService->track(
user: $user,
event: 'automated-reports-track-interest',
)
)->always();
return $this->response->withOk();
}
/**
* @throws ApplicationException
*/
public function list(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$teamIds = $request->has('team')
? (array) $request->get('team')
: [];
$reportTypes = $request->has('report_type')
? (array) $request->get('report_type')
: [];
$name = $request->has('name') ? trim($request->get('name', '')) : null;
try {
$fromDate = $request->has('from_date') ? Carbon::parse($request->get('from_date')) : null;
$toDate = $request->has('to_date') ? Carbon::parse($request->get('to_date')) : null;
} catch (\Exception) {
return $this->response->errorWrongArgs('Invalid date.');
}
$page = $request->has('page') ? (int) $request->get('page') : 1;
$sort = ReportSort::tryFrom(
$request->get(self::SORT_COLUMN, '')
) ?? ReportSort::GENERATED_AT;
$sortDirection = ReportSortDirection::tryFrom(
strtolower($request->get(self::SORT_DIRECTION, ''))
) ?? ReportSortDirection::DESC;
$paginatedUserReports = $this->automatedReportsRepository->getPaginatedUserReports(
user: $user,
sort: $sort,
sortDirection: $sortDirection,
resultsPerPage: self::RESULTS_PER_PAGE,
page: $page,
fromDate: $fromDate,
toDate: $toDate,
teamIds: array_map('intval', $teamIds),
reportTypes: $reportTypes,
name: $name,
);
$reportResults = $this->automatedReportsService->transformReportResults(
$paginatedUserReports->getCollection()
);
$team = $user->getTeam();
$reportTypeFilter = $this->automatedReportsService->getReportTypeFieldData(
shortVersion: true,
team: $team
);
$data = $this->apiResponseService->fromPaginatorToArray(
paginator: $paginatedUserReports,
data: $reportResults,
moreMeta: [
self::SORT_COLUMN => $sort->value,
self::SORT_DIRECTION => $sortDirection->value,
],
filters: [
$reportTypeFilter['id'] => $reportTypeFilter,
],
);
return $this->response->withArray($data);
}
public function delete(Request $request, string $uuid): JsonResponse
{
/** @var User $user */
$user = $request->user();
try {
$result = $this->automatedReportsRepository->findResultByUuidForUser($uuid, $user);
if ($result === null) {
return new JsonResponse(
data: ['error' => 'Report not found'],
status: JsonResponse::HTTP_NOT_FOUND
);
}
$result->delete();
return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
} catch (Throwable $e) {
return new JsonResponse(
data: ['error' => 'Failed to delete report result'],
status: JsonResponse::HTTP_INTERNAL_SERVER_ERROR
);
}
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
41
1
40
65
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
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 = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
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 = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
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 = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
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 = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
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 = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
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 = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
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 = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
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 = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
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 = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
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 = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
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 = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE 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;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
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 = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
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 = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
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 = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
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 = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
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 = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * 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");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
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 = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
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 = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
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 = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -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;...
|
58565
|
NULL
|
NULL
|
NULL
|
|
58565
|
2076
|
5
|
2026-05-19T12:54:20.438758+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195260438_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKERO ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKERO ₴1DEV (docker)₴82APP (-zs!remote:Counting objects:100% (2/2), done.Compressing objects: 100%remote: Total 2 (delta 0),(delta 0),Unpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.s, used o (from o)From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objectsorigin/JY-20676-dele*Merge made by theroutes/api.php 17 ++++1 file changed, 4 insertions(+),deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isCuse"gitahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.push"to publish your localcommits)Changes notstaged forcommit:use"gitadd<file>..."to update what willbecommitted)directory)PS$I31PhpStormWEBHOOK_FILTERING_IMPLEMENTATION.mdapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.phpapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.phpids.txtpublic/favicon.icoraw_sql_query.sqltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "git"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24,done.Counting objects: 100% (18/18),done.Deltacompression using up to 8 threadsCompressing objects: 100%(9/9),done.Writing objects: 100% (10/10), 1.01 KiB | 1.01 MiB/s, done.Total 10 (delta 7), reused 0 (delta 0), pack-reused 0remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob• •‹HomeDMsActivityED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-vousthepeople_of_Nvasil vasllev1 Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Appsi Jira CloudToast> 0.100% <78• Tue 19 May 15:54:19QDescribe what you are looking for# releases8 22• MessagesC Files• Bookmarks+177ea29b- Mequest #12102 fromTodayjiminny/JY-209~+ un u ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02LOLUIUULI LIVINILUUUUUUNIN/UUIIIUICALI[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
|
-1500638734409859427
|
NULL
|
visual_change
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKERO ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKERO ₴1DEV (docker)₴82APP (-zs!remote:Counting objects:100% (2/2), done.Compressing objects: 100%remote: Total 2 (delta 0),(delta 0),Unpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.s, used o (from o)From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objectsorigin/JY-20676-dele*Merge made by theroutes/api.php 17 ++++1 file changed, 4 insertions(+),deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isCuse"gitahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.push"to publish your localcommits)Changes notstaged forcommit:use"gitadd<file>..."to update what willbecommitted)directory)PS$I31PhpStormWEBHOOK_FILTERING_IMPLEMENTATION.mdapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.phpapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.phpids.txtpublic/favicon.icoraw_sql_query.sqltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "git"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24,done.Counting objects: 100% (18/18),done.Deltacompression using up to 8 threadsCompressing objects: 100%(9/9),done.Writing objects: 100% (10/10), 1.01 KiB | 1.01 MiB/s, done.Total 10 (delta 7), reused 0 (delta 0), pack-reused 0remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-ob• •‹HomeDMsActivityED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-vousthepeople_of_Nvasil vasllev1 Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Appsi Jira CloudToast> 0.100% <78• Tue 19 May 15:54:19QDescribe what you are looking for# releases8 22• MessagesC Files• Bookmarks+177ea29b- Mequest #12102 fromTodayjiminny/JY-209~+ un u ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02LOLUIUULI LIVINILUUUUUUNIN/UUIIIUICALI[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
|
|
58562
|
2077
|
4
|
2026-05-19T12:53:43.375989+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195223375_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
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:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>17 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
5252240045865077800
|
-8204424741936591930
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
PhostormVIewcodeLaravelFV faVsco.js?9 master kroledey© SyncPlanhat.php© [EMAIL]© AcuvityLeadConvertea.php© UserAutomatedReportsController.php xPlaybackController.phrphp.phpstorm.meta.php=.phpunit.result.cacheE .prettierignore= windsurtrules)Inteqrationapp/service.onoC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.phpphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.md© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk \AutomatedReports\ReportSortDirection;use Jiminny \Services\PlanhatService;comooser.isonuse ectumtnace hutp kequestcomooser.lockuse Inrowable,dependency-checker.json8 dev isonclass useraucomacedreporesconcrocler excenas controuten= ids tytl=infection.ison.distMLINSTAlI.mdpublic const int RESULTS PER_ PAGE = 25:MLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllpublic const string SORT COLUMN = 'sort column':M Makefilepackace-lock. sonE phpstan.neon.distpublic const strina SORT DIRECTION = "sort direction':= phostan-baseline.neon<> phpunit.xmlpublic function constructTe raw sal query.saprivate readonly AutomatedReportsRepository $automatedReportsRepository.private readonly AutomatedRenortsService sautomatedRenortsServicelM+ README.mdsô sonar-proiect propertiesE test.ovorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.‹> Untitled Diaaram.xmliprivate readonly PlanhatService $planhatServiceusvetur.confio.is1...M+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and ConsolespoST lanilv1lautomated-renorts/interest 1usadepublic function trackInterest(Request $request): JsonResponse(...}v batabase Consolesv AEU# console (EUl* Othnows ApplicationExceptionI DEAL RISKS (SU1A DITEUlGET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}A EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# Di [liminny@localhostlA HS local [iiminny@localhostlA SF ['iiminny@localhost]& zoho dev [iiminny@localhost]V & PRODA console [PROD]& console 1 [PROD158(09 >125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest, string Suuid): JsonResponsef...}LDIPROD577E| A14 X2 A Y $[CREDIT_CARD]% 5• Tue 19 May 15:53:43AskJiminnyReportActivityServiceTest v=laravel.logA SF [jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyCascadeA console [STAGING]Fix AutomatedReportsCascadeText Relay Duplication+0 ..CONCAT(U.Id, CASE WHEN U.1d = t.owner_Id THEN " (owner)" ELSE" END) AS USer_I141 41440 y.65 A Vcan i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a questionsa.*t.owner_id FROM social_accounts saNoun usens u on u.ide sa.sociable 1diTnoughtror isYes, you can keep both email addresses in the same domain:current setup:Jouin teams +n<»>i: on t.ide u.team1i• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)VHERE u.team id = 581 and sa.nroviden = Isalesforce!,Proposed setupSELECT * FROM automated_report_results order by id desc;catch-allatxt.iiminnv.com - used by US onlv• [EMAIL] - used by EU onlyselect * from featureslYou wouldcolont * fnom toam foatunoc whono foatuno id = 40.1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domainselect * from teams where id = 556:select * from automated_reports:where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044["pdf" "podcast"]SELECT * FROM automated_ report_results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:select * from automated_report_results order by id desc;SELECT * FROM automated_report_results WHERE id = 1919:Regarding using jiminny.com domainYes, you could use the main domain instead of the txt.iminny.com subdomain:catch-allajiminny.com for UScatch-all-euaiiminnv.com for EUhowever, this would require• Changing the existing txt.jiminny.com setup (which is currently working for US)select * from automated report results WHERE report id = 54:Micht artect existind users who have SMS nottications with @txt.liminnv.com reolv-to addressesselect * from opportunities where id = 7594349:• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comnnv.com. This is the least disruptiveSELECT * FROM teams WHERE name LIKE '%Les%': # 711, 692. [EMAIL] - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based filterina aporoach usina [EMAIL]?select * From mlavbooks where team 1d = 111. # event 226147SEE * SROM navhook catedories WHERE DLavbook 10 = 551511 ***SELECT * FROM eom fields WHERE eom confiauration 1d = 692 and obnect tvne = 'eventi.WHEREI1d = 2261471crm_field_values WHERE crm_field_id = 226147;SELECT * FROMIerm confiaurations WHERE 1d = 692CONCAT(u.id, CASE WHEN v.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,uemai]sa.*t.owner id FROM social accounts saJOIN users u on u.id = sa.sociable idIOTM toome + 1 necsi: on + 4d e m toom %dWHERE u.team id = 711 and sa.provider = 'salesforce' :SELECT * FROM crm profiles cp JOIN users u 1..n<->1: on u.id = co.user id WHERE u.team_id = 711:select * from leads:select * from calendarsves imolementceicortid AS team id"Codo CWE.1GW Windsurf Teams 594:60UTE.9Aensod...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58561
|
2076
|
4
|
2026-05-19T12:53:43.404599+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195223404_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
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
14...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>17 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"14","depth":4,"on_screen":true,"role_description":"text"}]...
|
2979094829499129611
|
-8204415945843561534
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1DEV (docker)₴82APP (-zslCounting objects: 100% (2/2), done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),reused 0 (delta 0), pack-reusedUnpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.1.2 packs, used 0 (from o)From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objects ->origin/JY-20676-dele*Merge made by theroutes/api.php 17++++---1 file changed, 4 insertions(+),3 deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.Cuse"git push"to publish your local commits)Changes notstaged for commit:(use"git add<file>..."to update what will becommitted)Cuse "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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "gitadd" and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24, done.Counting objects: 100% (18/18), done.Deltacompression using up to 8 threadsCompressing objects: 100% (9/9), done.Totin1o baects: 7, % s/10 ((e.e1 K), pack-rause/s, done.remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•MoreED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of _jimi...• Direct messagesRo Nikolay YankovD. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James Graham8 Stoyan TanevE Lukas Kovalik y... 0::: Apps|i Jira CloudToastall100% <73• Tue 19 May 15:53:43Describe 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
|
|
58556
|
2077
|
1
|
2026-05-19T12:53:22.978828+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195202978_m2.jpg...
|
PhpStorm
|
releases (Channel) - Jiminny Inc - 3 new items - S releases (Channel) - Jiminny Inc - 3 new items - Slack...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormroledeyFV faVsco.jsVIew?9 master kcodeLara PhostormroledeyFV faVsco.jsVIew?9 master kcodeLaravel100% 5• Tue 19 May 15:53:22AskJiminnyReportActivityServiceTest v© ConvertLeadActivities.phpC) SaveActivitv.oho© SyncPlanhat.php© [EMAIL]=.phounit.result.cacheE .prettierignore= windsurtrulesphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.mdcomooser.isoncomooser.lockdependency-checker.json8 dev ison= ids tytl=infection.ison.distMLINSTAlI.mdMLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllM Makefile1рackace-lock. sonE phpstan.neon.dist= phostan-baseline.neon<> phpunit.xmlTe raw sal query.saM+ README.mdsô sonar-proiect propertiesE test.ov‹> Untitled Diaaram.xmliusvetur.confio.isM+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and Consolesv batabase Consolesv AEU# console (EUlI DEAL RISKS (SU1A DITEUlA EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# DI liiminny@localhost]Checked out mactenfliminny@localhostlny@localhost]& zoho dev [iiminny@localhost]© AcuvityLeadConvertea.php)Inteqrationapp/service.ono© UserAutomatedReportsController.php xPlaybackController.phpC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;use Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,| A14 X2 ^ Y 578class useraucomacedreporesconcrocler excenas controuten=custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]« console [PROD] X# console [euyCascadeA console [STAGING]Fix AutomatedReportsCascadeText Relay Duplication+0 ..CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_i ™141 41 440 V65 A Vu.emailcan i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a question sa.*,t.owner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_id• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)WHERE u.team_id = 581 and sa.provider = 'salesforce' :SELECT * FROM automated_ report results order by id desc:select * from features:select * from team_features where feature_ id = 40:Tnoughtror isYes, you can keep both email addresses in the same domain:current setup:Proposed setupcatch-allatxt.iiminnv.com - used by US onlv• [EMAIL] - used by EU onlyYou would1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domainselect * from teams where id = 556:public const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository,private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lani/v1lautomated-renorts/interest 1usaaepublic function trackInterest(Request Srequest): JsonResponsef...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}V & PRODA console [PROD]& console 1 [PROD1LDIPROD BBаdEaRa-611612. 613where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044 ["odf"_ "oodcast"]]SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;select * From automated renort results order bv id desc.SELECT * FROM automated_report_results WHERE id = 1919;Regarding using jiminny.com domainYes, you could use the main domain instead of the txt.iminny.com subdomain:catch-allajiminny.com for UScatch-all-euaiiminnv.com for EUselect * from automated_report_results WHERE report id = 54;however, this would require• Changing the existing txt.jiminny.com setup (which is currently working for US)Micht artect existind users who have SMS nottications with @txt.liminnv.com reolv-to addresses• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comselect * from opportunities where id = 7594349;Recommendation: Keep the txt.liminnv.com domain and just add [EMAIL]. This is the least disruptiveSELECT * FROM teams WHERE name LIKE '%Les%': # 711, 692, 16067 - [EMAIL] - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based filterina aporoach usina [EMAIL]?splect * from nlavhooks whene team id = 711. # event 226147SELECT * FROM playbook_categories WHERE playbook_id = 5515;1 ***SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':SELECT * FROM crm_fields WHERE id = 226147:cciseT + CD0M enm fiold vnluoc WHEDE enm 4io1d ¿d = 224147.SELECT * FROM crm_configurations WHERE id = 692SELECTCONCAT(u.id, CASE WHEN u.id = t.owner id THEN ' (owner)' ELSE •• END) AS user idv.emailsa.*,t.owner_id FROM social accounts saJOIN users u on u.id = sa.sociable idJOIN teans + 1.n<->1: on t.id = u.tean idWHERE u,team id = 711 and sa.providen = 'salesforce'.SELECT * FROM crm profiles co J0IN users u 1..n<->1: on u.id = co.user id WHERE u.team id = 711:splect * From eads,splect * from calendans.SELECTves imolement"Codo CWE.1G+id AS team idlW Windsurf Teams 594:60UTE.9Aensod...
|
NULL
|
-1593178576280908188
|
NULL
|
app_switch
|
ocr
|
NULL
|
PhostormroledeyFV faVsco.jsVIew?9 master kcodeLara PhostormroledeyFV faVsco.jsVIew?9 master kcodeLaravel100% 5• Tue 19 May 15:53:22AskJiminnyReportActivityServiceTest v© ConvertLeadActivities.phpC) SaveActivitv.oho© SyncPlanhat.php© [EMAIL]=.phounit.result.cacheE .prettierignore= windsurtrulesphpde neloer.onophpide heloer_models.ohophp artisanM'CLAUDE.mdcomooser.isoncomooser.lockdependency-checker.json8 dev ison= ids tytl=infection.ison.distMLINSTAlI.mdMLINTERNAI WERHOOK SFTUP mdEjiminny_storageMiliconsos mdllM Makefile1рackace-lock. sonE phpstan.neon.dist= phostan-baseline.neon<> phpunit.xmlTe raw sal query.saM+ README.mdsô sonar-proiect propertiesE test.ov‹> Untitled Diaaram.xmliusvetur.confio.isM+ WEBHOOK FILTERING_IMPLEMENTATION.mo>1h External Librariesv =° Scratches and Consolesv batabase Consolesv AEU# console (EUlI DEAL RISKS (SU1A DITEUlA EU (EU]v Ajiminny@localhostA console (jiminny@localhost]# DI liiminny@localhost]Checked out mactenfliminny@localhostlny@localhost]& zoho dev [iiminny@localhost]© AcuvityLeadConvertea.php)Inteqrationapp/service.ono© UserAutomatedReportsController.php xPlaybackController.phpC) CreateCommentedEvent.php(C) CreateSmsSentEvent.phpC) PlanhatActivityListener.php© AskAnythingPromptService.php(C) AutomatedReportsRepositorv.php© AutomatedReportsCommand.phpphp api_v2.phpC) RequestGenerateReportJob.php© AutomatedReportResult.php(C) AutomatedReport.ohouse Jiminny \Services \Kiosk\AutomatedReports \ReportSortDirection;use Jiminny \Services\PlanhatService;use ectumtnace hutp kequestuse Inrowable,| A14 X2 ^ Y 578class useraucomacedreporesconcrocler excenas controuten=custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]« console [PROD] X# console [euyCascadeA console [STAGING]Fix AutomatedReportsCascadeText Relay Duplication+0 ..CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_i ™141 41 440 V65 A Vu.emailcan i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a question sa.*,t.owner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_id• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)WHERE u.team_id = 581 and sa.provider = 'salesforce' :SELECT * FROM automated_ report results order by id desc:select * from features:select * from team_features where feature_ id = 40:Tnoughtror isYes, you can keep both email addresses in the same domain:current setup:Proposed setupcatch-allatxt.iiminnv.com - used by US onlv• [EMAIL] - used by EU onlyYou would1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domainselect * from teams where id = 556:public const int RESULTS PER_ PAGE = 25:public const string SORT COLUMN = 'sort column':public const strina SORT DIRECTION = "sort direction':public function constructprivate readonly AutomatedReportsRepository $automatedReportsRepository,private readonly AutomatedRenortsService SautomatedRenortsServicelorivate readoniv AniResponseService samiResponseServi.ce.orivate readoni.v Resnonse Sresnonse.private readonly PlanhatService $planhatService1...poST lani/v1lautomated-renorts/interest 1usaaepublic function trackInterest(Request Srequest): JsonResponsef...}* Othnows ApplicationException58(09 >GET /api/v1/automated-reportspublic function list(Request Srequest): JsonResponse(...}125 0)DELETE /api/vl/automated-reports/kuuidpublic function delete(Request Srequest. string Suuid): JsonResponse(...}V & PRODA console [PROD]& console 1 [PROD1LDIPROD BBаdEaRa-611612. 613where id = 54: # 4fdd41f6-dcf0-30d0-b339-7345381b6044 ["odf"_ "oodcast"]]SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;select * From automated renort results order bv id desc.SELECT * FROM automated_report_results WHERE id = 1919;Regarding using jiminny.com domainYes, you could use the main domain instead of the txt.iminny.com subdomain:catch-allajiminny.com for UScatch-all-euaiiminnv.com for EUselect * from automated_report_results WHERE report id = 54;however, this would require• Changing the existing txt.jiminny.com setup (which is currently working for US)Micht artect existind users who have SMS nottications with @txt.liminnv.com reolv-to addresses• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comselect * from opportunities where id = 7594349;Recommendation: Keep the txt.liminnv.com domain and just add [EMAIL]. This is the least disruptiveSELECT * FROM teams WHERE name LIKE '%Les%': # 711, 692, 16067 - [EMAIL] - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based filterina aporoach usina [EMAIL]?splect * from nlavhooks whene team id = 711. # event 226147SELECT * FROM playbook_categories WHERE playbook_id = 5515;1 ***SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':SELECT * FROM crm_fields WHERE id = 226147:cciseT + CD0M enm fiold vnluoc WHEDE enm 4io1d ¿d = 224147.SELECT * FROM crm_configurations WHERE id = 692SELECTCONCAT(u.id, CASE WHEN u.id = t.owner id THEN ' (owner)' ELSE •• END) AS user idv.emailsa.*,t.owner_id FROM social accounts saJOIN users u on u.id = sa.sociable idJOIN teans + 1.n<->1: on t.id = u.tean idWHERE u,team id = 711 and sa.providen = 'salesforce'.SELECT * FROM crm profiles co J0IN users u 1..n<->1: on u.id = co.user id WHERE u.team id = 711:splect * From eads,splect * from calendans.SELECTves imolement"Codo CWE.1G+id AS team idlW Windsurf Teams 594:60UTE.9Aensod...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58555
|
2076
|
1
|
2026-05-19T12:53:22.978575+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195202978_m1.jpg...
|
PhpStorm
|
releases (Channel) - Jiminny Inc - 3 new items - S releases (Channel) - Jiminny Inc - 3 new items - Slack...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKERO ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKERO ₴1DEV (docker)₴82APP (-zs!remote:Counting objects:100% (2/2),done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),(delta 0),Unpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objectsorigin/JY-20676-dele*Merge made by theroutes/api.php 17 ++++1 file changed, 4 insertions(+),deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branchisCuse"gitahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.push"to publish your localcommits)Changes notstaged forcommit:use"gitadd‹file>..."to update what willbecommitted)PS3$I31SlackWEBHOOK_FILTEKING_IMPLEMEN1AI1UN.maapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.phpapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.phpids.txtpublic/favicon.icoraw_sql_query.sqltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "gitadd"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24,done.Counting objects: 100% (18/18),done.Deltacompression using up to 8 threadsCompressing objects: 100%(9/9),done.Writing objects: 100% (10/10), 1.01 KiB | 1.01 MiB/s, done.Total 10 (delta 7), reused 0 (delta 0), pack-reused 0remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-repollukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-vousNAneliya Angelova8. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan TanevLukas Kovalik y...::: Appsi Jira CloudToast100% C78• Tue 19 May 15:53:22QDescribe what you are looking for# releases8 22• MessagesC Files• Bookmarks+177ea29b - Mequest #12102 fromTodayjiminny/JY-209~+ un u ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02LOLUIUULI LIVIVILUUUUUUNINIVUII LUICOLI[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
|
7566043522621961818
|
NULL
|
app_switch
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKERO ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKERO ₴1DEV (docker)₴82APP (-zs!remote:Counting objects:100% (2/2),done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),(delta 0),Unpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objectsorigin/JY-20676-dele*Merge made by theroutes/api.php 17 ++++1 file changed, 4 insertions(+),deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branchisCuse"gitahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.push"to publish your localcommits)Changes notstaged forcommit:use"gitadd‹file>..."to update what willbecommitted)PS3$I31SlackWEBHOOK_FILTEKING_IMPLEMEN1AI1UN.maapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.phpapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.phpids.txtpublic/favicon.icoraw_sql_query.sqltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "gitadd"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24,done.Counting objects: 100% (18/18),done.Deltacompression using up to 8 threadsCompressing objects: 100%(9/9),done.Writing objects: 100% (10/10), 1.01 KiB | 1.01 MiB/s, done.Total 10 (delta 7), reused 0 (delta 0), pack-reused 0remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-repollukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-vousNAneliya Angelova8. Stefka Stoyanovao Stoyan Tomove Todor Stamatov "Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan TanevLukas Kovalik y...::: Appsi Jira CloudToast100% C78• Tue 19 May 15:53:22QDescribe what you are looking for# releases8 22• MessagesC Files• Bookmarks+177ea29b - Mequest #12102 fromTodayjiminny/JY-209~+ un u ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02LOLUIUULI LIVIVILUUUUUUNINIVUII LUICOLI[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
|
|
58542
|
2075
|
9
|
2026-05-19T12:51:09.271145+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779195069271_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Workspace associated with branch 'master' Workspace associated with branch 'master' has been restored
text/html
text/html
text/html
Rollback
Configure…
More
Checked out master...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"Workspace associated with branch 'master' has been restored","depth":3,"bounds":{"left":0.8753325,"top":0.915403,"width":0.11037234,"height":0.027134877},"on_screen":true,"value":"Workspace associated with branch 'master' has been restored","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.915403,"width":0.0987367,"height":0.027134877},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Rollback","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.017287234,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure…","depth":2,"bounds":{"left":0.89793885,"top":0.9481245,"width":0.023603724,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"More","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.016289894,"height":0.0},"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Checked out master","depth":3,"bounds":{"left":0.016954787,"top":0.8595371,"width":0.04488032,"height":0.016759777},"on_screen":true,"value":"Checked out master","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3841374697011603329
|
-5471011422982447538
|
app_switch
|
accessibility
|
NULL
|
Workspace associated with branch 'master' Workspace associated with branch 'master' has been restored
text/html
text/html
text/html
Rollback
Configure…
More
Checked out master...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58508
|
2072
|
8
|
2026-05-19T12:44:53.378556+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194693378_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1 SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1DEV (docker)₴82APP (-zslCounting objects: 100% (2/2), done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),reused 0 (delta 0), pack-reusedUnpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objects ->origin/JY-20676-dele*Merge made by theroutes/api.php 17++++---1 file changed, 4 insertions(+),3 deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.Cuse"git push"to publish your local commits)Changes notstaged for commit:(use"git add<file>..."to update what will becommitted)Cuse "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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges added to commit (use "gitadd" and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24, done.Counting objects: 100% (18/18), done.Deltacompression using up to 8 threadsCompressing objects: 100% (9/9), done.Totin1o baects: 7, % s/10 ((e.e1 K), pack-rause/s, done.remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•MoreED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesRo Nikolay YankovD. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Apps|Jira CloudToast> 0.all100% <73• Tue 19 May 15:44:53Describe what you are looking forToastHomeMessagesAbout@Vasil VasilevYesterday~approved by youToday ~Toast APP 10:00 AMMergeapp#12092 Allow owner's role to beselected when setting up a trial19 hours old • 6 files changedAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubReview Toast APP 11:51 AM#518 JY-20919: Lower exee reports batch size by@Steliyan Georgievmerged by @Steliyan Georgiev (author) after approval bynikolaybiaivanovMessage Toast+Aa...
|
NULL
|
-7947004878134635147
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1 SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1DEV (docker)₴82APP (-zslCounting objects: 100% (2/2), done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),reused 0 (delta 0), pack-reusedUnpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objects ->origin/JY-20676-dele*Merge made by theroutes/api.php 17++++---1 file changed, 4 insertions(+),3 deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.Cuse"git push"to publish your local commits)Changes notstaged for commit:(use"git add<file>..."to update what will becommitted)Cuse "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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges added to commit (use "gitadd" and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24, done.Counting objects: 100% (18/18), done.Deltacompression using up to 8 threadsCompressing objects: 100% (9/9), done.Totin1o baects: 7, % s/10 ((e.e1 K), pack-rause/s, done.remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•MoreED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesRo Nikolay YankovD. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Apps|Jira CloudToast> 0.all100% <73• Tue 19 May 15:44:53Describe what you are looking forToastHomeMessagesAbout@Vasil VasilevYesterday~approved by youToday ~Toast APP 10:00 AMMergeapp#12092 Allow owner's role to beselected when setting up a trial19 hours old • 6 files changedAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubReview Toast APP 11:51 AM#518 JY-20919: Lower exee reports batch size by@Steliyan Georgievmerged by @Steliyan Georgiev (author) after approval bynikolaybiaivanovMessage Toast+Aa...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58507
|
2073
|
7
|
2026-05-19T12:44:53.068626+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194693068_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.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
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - Team A\B\C',
'ex...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.6449468,"top":0.92098963,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.6449468,"top":0.952913,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>17 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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":"14","depth":4,"bounds":{"left":0.37466756,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.3863032,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"73","depth":4,"bounds":{"left":0.40026596,"top":0.15003991,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\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 = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\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 = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.42652926,"top":0.9577015,"width":0.29321808,"height":0.013567438},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","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}]...
|
-7089939043104703802
|
-1412888448243747103
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - Team A\B\C',
'ex...
|
58505
|
NULL
|
NULL
|
NULL
|
|
58506
|
2072
|
7
|
2026-05-19T12:44:50.649396+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194690649_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Checkout
New Branch from 'master'…
Checkou Checkout
New Branch from 'master'…
Checkout and Rebase onto 'JY-20676-delete-report-related-objects'...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Checkout","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.027777778,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"New Branch from 'master'…","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.027777778,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Checkout and Rebase onto 'JY-20676-delete-report-related-objects'","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.027777778,"height":0.024444444},"on_screen":false,"role_description":"text"}]...
|
-3488309367639132711
|
2524429789193969928
|
click
|
accessibility
|
NULL
|
Checkout
New Branch from 'master'…
Checkou Checkout
New Branch from 'master'…
Checkout and Rebase onto 'JY-20676-delete-report-related-objects'...
|
58503
|
NULL
|
NULL
|
NULL
|
|
58505
|
2073
|
6
|
2026-05-19T12:44:50.086223+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194690086_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Checkout
New Branch from 'master'…
Checkou Checkout
New Branch from 'master'…
Checkout and Rebase onto 'JY-20676-delete-report-related-objects'
Checkout and Update
Compare with 'JY-20676-delete-report-related-objects'
Show Diff with Working Tree
Rebase 'JY-20676-delete-report-related-objects' onto 'master'
Checkout...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Checkout","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.013297873,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"New Branch from 'master'…","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.013297873,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Checkout and Rebase onto 'JY-20676-delete-report-related-objects'","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.013297873,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Checkout and Update","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.013297873,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compare with 'JY-20676-delete-report-related-objects'","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.013297873,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Show Diff with Working Tree","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.013297873,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Rebase 'JY-20676-delete-report-related-objects' onto 'master'","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.013297873,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Checkout","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.013297873,"height":0.0},"on_screen":false,"role_description":"text"}]...
|
6047873101554593882
|
8699013316331381386
|
visual_change
|
accessibility
|
NULL
|
Checkout
New Branch from 'master'…
Checkou Checkout
New Branch from 'master'…
Checkout and Rebase onto 'JY-20676-delete-report-related-objects'
Checkout and Update
Compare with 'JY-20676-delete-report-related-objects'
Show Diff with Working Tree
Rebase 'JY-20676-delete-report-related-objects' onto 'master'
Checkout...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58504
|
2073
|
5
|
2026-05-19T12:44:47.105535+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194687105_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormcodeFV faVsco.jsProiect© ResolveCompanyNam PhostormcodeFV faVsco.jsProiect© ResolveCompanyNameByEmailTraitTe© TimePerioditeratorTest.php© UpdateCrmDataResolverTest.php› Internalv C Kioskv _ Automated Reports© ActivitvTvpeServiceTest.ohp(C) AskJiminnvReoortActivitvServiceTel(C) AskJiminnvReoortServiceTest.ono(C) AutomatedReportsCallbackServicel(C) AutomatedReportsServiceActivities(C) AutomatedReportsServiceExtendedc) AutomatedRenortsServiceSieldbat:(c) AutomatedRenortsServiceSormattin(C) AutomatedRenortsServiceGetActivi© AutomatedReportsServiceGetPlayb©) AutomatedReportsServiceReportGe© AutomatedReportsServiceTest.php(eh AutamatodDonortcCorvicoValidator(eh AutamatodDonortcCorvicoValidatiol© RecipientServiceTest.php› C Mai> C MeetingGenerator> 0 Notification[ RecallAlD Team_ lelephonyUserPillot1 Webhook©) ActivitvServiceTest.ohoC ApiResponseServiceTest.phpC InstantMeetinaServiceTest.pho@* IntercomServiceTest.php@ KioskServiceTest.ohnC PlanhatService Test.nho@ PlavbackVideoOnlyServiceTest.php© PlaybookCategoryServiceTest.php(c) ResolveTeamCrmConnectionTest.nho© SlackServiceTest.phpM CocialA ccountCorviceTact nhn© TeamDeactivatedServiceTest.php© TeamOwnerServiceTest.php© TeamServiceTest.php© UserServiceTest.php> Co Traits> C UseCases> C Usen> C Utils> D Validation100% 5• Tue 19 May 15:44:47+0 ..© AskAnythingController.phpcustom.log= laravel.log#SF liiminnv@localhostlX4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.php X# concole ISTAGinG1C) CoachinaFeedhackCoachl.Icerin.nhn© AskAnythingPromptDto.php© AskJiminnyReportsController.phg©) AskAnythingPromptServiceTest.php© Search.php207class AutomatedReportsservicelest extends Testcase190 % >201 %247 %257 %297 YlA14 A102 У73 ^ v 208#DataProvider('transformMed1alypesDataProv1der')Jpublic function testTransformMediaTypes(array SmediaTypes, array Sexpected): void{...}59. 217212public function testGetMediaTypeFieldDataWithoutReport(): voidf...}=214public function testGetMediaTypeFieldDataWithReport(): void{...}2141 usagepublic static function transformMediaTvnesDataProvider()• arravf…..}-218=219= 220#[DataProvider('hasCallTypeConferenceDataProvider')]public function testHasCallTypeConference(array $callTypes, bool Sexpected): void{...}221=222=223#[DataProvider('hasCallTypeDialerDataProvider')]public function testHasCallTypeDialer(array $callTypes, bool $expected): void{...}2251 usage-221public static function hasCallTypeConferenceDataProvider(): array{...}— 229229=230public static function hasCallTypeDialerDataProvider®: array{...}231public function testTransformReportResultsWithEmptyCollection0: void{...}2253public function testTransformReportResultsStructure@: void{...}235public function testTransformReportResultsWithMultipleResults@: voidf...}#DataProvider(isuserRecioient0fReportDataProviden'ii239public function testIsUserRecipient0fReport(int $userId, array $recipients, bool $expected): voidf...— 240#[DataProvider('isUserRecipient0fAskJiminnyReportDataProvider')]nublie function testIsllserRecinient0fAsk.liminnvRenontViaGroun^l243int Susentd.2int SarounTd.=245array Srecipients,=246array SreportGroups,bool $expected,): void {...}public static function isUserRecipient0fAskJiminnyReportDataProvider(= arnavfSo jiminny vm 021 A1 A18 V2 V6 ^ Vreports where id = 67;reports where id = 42;E id = 143: # group 28e id = 3143urations where id = 500:e name = 'Integration Account': # 1695ounts WHERE sociable id = 1695:where crm_configuration_ id = 39ecorded' and duration > 60and actual start time >= 12025-12-01'-WHERE uuid to bind'458cf915-6914-4000-6083-5687632629561 = uuid:WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003WHERF id TN (16.422003)-whenp status = Ifailedt.PE activitv id = 422003•Anconid = n.idf1acb-16b8-42d1-8726-df52979dad0e') = U.uuidacks tid = a.id"video')time DESdeid = 19.unations where nroviden = Ininednive!.a1 secnunte caSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedCascadeFix AutomatedReportsCascadeText Relay Duplicationcan i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a questionYes, you can keep both email addresses in the same domain:current setup:• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)Proposed setup• [EMAIL] - used by US only• [EMAIL] - used by EU onlyYou would1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domain3. Update EU contig to use catch-all-eu mailboxReaardina usina liminnv.com domainYes, you could use the main domain instead of the txt.jiminny.com subdomain:catch-allajiminny.com for US• [EMAIL] for EUhowever, this would recuire• Changing the existing txt.jiminny.com setup (which is currently working for US)• Micht aftect existina users who have SMS nottications with @txt.liminnv.com reolv-to addresses• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comRecommendation: Keep the txt.liminnv.com domain and just add [EMAIL]. This is the least disruptivechange - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based filterina aporoach usina [EMAIL]?1 ***« Code SWF-1.6WN Windsurf Teamsio 4 spaces...
|
NULL
|
-905049563248488989
|
NULL
|
click
|
ocr
|
NULL
|
PhostormcodeFV faVsco.jsProiect© ResolveCompanyNam PhostormcodeFV faVsco.jsProiect© ResolveCompanyNameByEmailTraitTe© TimePerioditeratorTest.php© UpdateCrmDataResolverTest.php› Internalv C Kioskv _ Automated Reports© ActivitvTvpeServiceTest.ohp(C) AskJiminnvReoortActivitvServiceTel(C) AskJiminnvReoortServiceTest.ono(C) AutomatedReportsCallbackServicel(C) AutomatedReportsServiceActivities(C) AutomatedReportsServiceExtendedc) AutomatedRenortsServiceSieldbat:(c) AutomatedRenortsServiceSormattin(C) AutomatedRenortsServiceGetActivi© AutomatedReportsServiceGetPlayb©) AutomatedReportsServiceReportGe© AutomatedReportsServiceTest.php(eh AutamatodDonortcCorvicoValidator(eh AutamatodDonortcCorvicoValidatiol© RecipientServiceTest.php› C Mai> C MeetingGenerator> 0 Notification[ RecallAlD Team_ lelephonyUserPillot1 Webhook©) ActivitvServiceTest.ohoC ApiResponseServiceTest.phpC InstantMeetinaServiceTest.pho@* IntercomServiceTest.php@ KioskServiceTest.ohnC PlanhatService Test.nho@ PlavbackVideoOnlyServiceTest.php© PlaybookCategoryServiceTest.php(c) ResolveTeamCrmConnectionTest.nho© SlackServiceTest.phpM CocialA ccountCorviceTact nhn© TeamDeactivatedServiceTest.php© TeamOwnerServiceTest.php© TeamServiceTest.php© UserServiceTest.php> Co Traits> C UseCases> C Usen> C Utils> D Validation100% 5• Tue 19 May 15:44:47+0 ..© AskAnythingController.phpcustom.log= laravel.log#SF liiminnv@localhostlX4 HS_local [jiminny@localhost]& console [PROD]# console [euy© AskAnythingPromptService.php© AutomatedReportsServiceTest.php X# concole ISTAGinG1C) CoachinaFeedhackCoachl.Icerin.nhn© AskAnythingPromptDto.php© AskJiminnyReportsController.phg©) AskAnythingPromptServiceTest.php© Search.php207class AutomatedReportsservicelest extends Testcase190 % >201 %247 %257 %297 YlA14 A102 У73 ^ v 208#DataProvider('transformMed1alypesDataProv1der')Jpublic function testTransformMediaTypes(array SmediaTypes, array Sexpected): void{...}59. 217212public function testGetMediaTypeFieldDataWithoutReport(): voidf...}=214public function testGetMediaTypeFieldDataWithReport(): void{...}2141 usagepublic static function transformMediaTvnesDataProvider()• arravf…..}-218=219= 220#[DataProvider('hasCallTypeConferenceDataProvider')]public function testHasCallTypeConference(array $callTypes, bool Sexpected): void{...}221=222=223#[DataProvider('hasCallTypeDialerDataProvider')]public function testHasCallTypeDialer(array $callTypes, bool $expected): void{...}2251 usage-221public static function hasCallTypeConferenceDataProvider(): array{...}— 229229=230public static function hasCallTypeDialerDataProvider®: array{...}231public function testTransformReportResultsWithEmptyCollection0: void{...}2253public function testTransformReportResultsStructure@: void{...}235public function testTransformReportResultsWithMultipleResults@: voidf...}#DataProvider(isuserRecioient0fReportDataProviden'ii239public function testIsUserRecipient0fReport(int $userId, array $recipients, bool $expected): voidf...— 240#[DataProvider('isUserRecipient0fAskJiminnyReportDataProvider')]nublie function testIsllserRecinient0fAsk.liminnvRenontViaGroun^l243int Susentd.2int SarounTd.=245array Srecipients,=246array SreportGroups,bool $expected,): void {...}public static function isUserRecipient0fAskJiminnyReportDataProvider(= arnavfSo jiminny vm 021 A1 A18 V2 V6 ^ Vreports where id = 67;reports where id = 42;E id = 143: # group 28e id = 3143urations where id = 500:e name = 'Integration Account': # 1695ounts WHERE sociable id = 1695:where crm_configuration_ id = 39ecorded' and duration > 60and actual start time >= 12025-12-01'-WHERE uuid to bind'458cf915-6914-4000-6083-5687632629561 = uuid:WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003WHERF id TN (16.422003)-whenp status = Ifailedt.PE activitv id = 422003•Anconid = n.idf1acb-16b8-42d1-8726-df52979dad0e') = U.uuidacks tid = a.id"video')time DESdeid = 19.unations where nroviden = Ininednive!.a1 secnunte caSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refusedCascadeFix AutomatedReportsCascadeText Relay Duplicationcan i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a questionYes, you can keep both email addresses in the same domain:current setup:• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)Proposed setup• [EMAIL] - used by US only• [EMAIL] - used by EU onlyYou would1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domain3. Update EU contig to use catch-all-eu mailboxReaardina usina liminnv.com domainYes, you could use the main domain instead of the txt.jiminny.com subdomain:catch-allajiminny.com for US• [EMAIL] for EUhowever, this would recuire• Changing the existing txt.jiminny.com setup (which is currently working for US)• Micht aftect existina users who have SMS nottications with @txt.liminnv.com reolv-to addresses• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comRecommendation: Keep the txt.liminnv.com domain and just add [EMAIL]. This is the least disruptivechange - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based filterina aporoach usina [EMAIL]?1 ***« Code SWF-1.6WN Windsurf Teamsio 4 spaces...
|
58501
|
NULL
|
NULL
|
NULL
|
|
58503
|
2072
|
6
|
2026-05-19T12:44:49.135520+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194689135_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Update Project...
Commit…
Push…
New Branch…
Checko Update Project...
Commit…
Push…
New Branch…
Checkout Tag or Revision…
Recent
JY-20676-delete-report-related-objects
master
JY-20613-allow-owner-role-on-team-setup
JY-20725-handle-HS-search-rate-limit
pipedrive-sdk-poc
JY-20676-delete-report-related-objects
master
JY-20613-allow-owner-role-on-team-setup
JY-20725-handle-HS-search-rate-limit
pipedrive-sdk-poc
Local
Remote
Tags
Search
Fetch
Settings...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Update Project...","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Commit…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Push…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"New Branch…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Checkout Tag or Revision…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Recent","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20676-delete-report-related-objects","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"master","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20613-allow-owner-role-on-team-setup","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20725-handle-HS-search-rate-limit","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"pipedrive-sdk-poc","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20676-delete-report-related-objects","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"master","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20613-allow-owner-role-on-team-setup","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20725-handle-HS-search-rate-limit","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"pipedrive-sdk-poc","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Local","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Remote","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Tags","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"Search","depth":1,"on_screen":true,"help_text":"Search for branches and actions","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Fetch","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
173755226590339472
|
-2113120566414721394
|
click
|
accessibility
|
NULL
|
Update Project...
Commit…
Push…
New Branch…
Checko Update Project...
Commit…
Push…
New Branch…
Checkout Tag or Revision…
Recent
JY-20676-delete-report-related-objects
master
JY-20613-allow-owner-role-on-team-setup
JY-20725-handle-HS-search-rate-limit
pipedrive-sdk-poc
JY-20676-delete-report-related-objects
master
JY-20613-allow-owner-role-on-team-setup
JY-20725-handle-HS-search-rate-limit
pipedrive-sdk-poc
Local
Remote
Tags
Search
Fetch
Settings...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58502
|
2072
|
5
|
2026-05-19T12:44:47.065944+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194687065_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1 SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1DEV (docker)₴82APP (-zslCounting objects: 100% (2/2), done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),reused 0 (delta 0), pack-reusedUnpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objects ->origin/JY-20676-dele*Merge made by theroutes/api.php 17++++---1 file changed, 4 insertions(+),3 deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.Cuse"git push"to publish your local commits)Changes notstaged for commit:(use"git add<file>..."to update what will becommitted)Cuse "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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges added to commit (use "gitadd" and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24, done.Counting objects: 100% (18/18), done.Deltacompression using up to 8 threadsCompressing objects: 100% (9/9), done.Totin1o baects: 7, % s/10 ((e.e1 K), pack-rause/s, done.remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•MoreED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesRo Nikolay YankovD. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Apps|Jira CloudToast> 0.all100% <73• Tue 19 May 15:44:46Describe what you are looking forToastHomeMessagesAbout@Vasil VasilevYesterday~approved by youToday ~Toast APP 10:00 AMMergeapp#12092 Allow owner's role to beselected when setting up a trial19 hours old • 6 files changedAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubReview Toast APP 11:51 AM#518 JY-20919: Lower exee reports batch size by@Steliyan Georgievmerged by @Steliyan Georgiev (author) after approval bynikolaybiaivanovMessage Toast+Aa...
|
NULL
|
-2580017219123163524
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1 SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1DEV (docker)₴82APP (-zslCounting objects: 100% (2/2), done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),reused 0 (delta 0), pack-reusedUnpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objects ->origin/JY-20676-dele*Merge made by theroutes/api.php 17++++---1 file changed, 4 insertions(+),3 deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.Cuse"git push"to publish your local commits)Changes notstaged for commit:(use"git add<file>..."to update what will becommitted)Cuse "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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges added to commit (use "gitadd" and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24, done.Counting objects: 100% (18/18), done.Deltacompression using up to 8 threadsCompressing objects: 100% (9/9), done.Totin1o baects: 7, % s/10 ((e.e1 K), pack-rause/s, done.remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•MoreED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesRo Nikolay YankovD. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Apps|Jira CloudToast> 0.all100% <73• Tue 19 May 15:44:46Describe what you are looking forToastHomeMessagesAbout@Vasil VasilevYesterday~approved by youToday ~Toast APP 10:00 AMMergeapp#12092 Allow owner's role to beselected when setting up a trial19 hours old • 6 files changedAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubReview Toast APP 11:51 AM#518 JY-20919: Lower exee reports batch size by@Steliyan Georgievmerged by @Steliyan Georgiev (author) after approval bynikolaybiaivanovMessage Toast+Aa...
|
58500
|
NULL
|
NULL
|
NULL
|
|
58501
|
2073
|
4
|
2026-05-19T12:44:39.778208+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194679778_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6966022010367698874
|
-564708849257816637
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
PhostormProiectcodeFV faVsco.js?9 JY-20676-delete-report-related-objectscustom.log= laravel.log#SF liiminnv@localhostlX4 HS_local [jiminny@localhost]& console [PROD]# console [euy© ResolveCompanyNameByEmailTraitTe© AskAnythingPromptService.php© TimePerioditeratorTest.php© AskAnythingPromptDto.php© UpdateCrmDataResolverTest.php› Internal© AskJiminnyReportsController.phgv C Kioskv _ Automated ReportsC ActivitvTvpeServiceTest.oho© AutomatedReportsServiceTest.php X# concole ISTAGinG1C) CoachinaFeedhackCoachl.Icerin.nhn©) AskAnythingPromptServiceTest.php© Search.php207class AutomatedReportsservicelest extends Testcase(C) AskJiminnvReoortActivitvServiceTel(C) AskJiminnvReoortServiceTest.ono(C) AutomatedReportsCallbackServicel(C) AutomatedReportsServiceActivities(C) AutomatedReportsServiceExtendedc) AutomatedRenortsServiceSieldbat:(c) AutomatedRenortsServiceSormattin(C) AutomatedRenortsServiceGetActivi© AutomatedReportsServiceGetPlayb©) AutomatedReportsServiceReportGe© AutomatedReportsServiceTest.php(eh AutamatodDonortcCorvicoValidator190 % >(eh AutamatodDonortcCorvicoValidatiol© RecipientServiceTest.php› C Mai> C MeetingGenerator> 0 Notification[ RecallAlD Team_ lelephony201 %UserPillot1 Webhook©) ActivitvServiceTest.ohoC ApiResponseServiceTest.phpC InstantMeetinaServiceTest.pho@* IntercomServiceTest.php@ KioskServiceTest.ohnC PlanhatService Test.nho@ PlavbackVideoOnlyServiceTest.php© PlaybookCategoryServiceTest.php(c) ResolveTeamCrmConnectionTest.nho© SlackServiceTest.phpM CocialA ccountCorviceTact nhn247 %257 %297 Yl© TeamDeactivatedServiceTest.php© TeamOwnerServiceTest.php© TeamServiceTest.php© UserServiceTest.php> Co Traits> C UseCases> C Usen> C Utils> D ValidationA14 A102 У73 ^ v 208#DataProvider('transformMed1alypesDataProv1der')Jpublic function testTransformMediaTypes(array SmediaTypes, array Sexpected): void{...}public function testGetMediaTypeFieldDataWithoutReport(): voidf...}:8. 212212=214public function testGetMediaTypeFieldDataWithReport(): void{...}2141 usagepublic static function transformMediaTvnesDataProvider()• arravf…..}#[DataProvider('hasCallTypeConferenceDataProvider')]public function testHasCallTypeConference(array $callTypes, bool Sexpected): void{...}#[DataProvider('hasCallTypeDialerDataProvider')]public function testHasCallTypeDialer(array $callTypes, bool $expected): void{...}-218=219= 220221=222_2232251 usagepublic static function hasCallTypeConferenceDataProvider(): array{...}public static function hasCallTypeDialerDataProvider0: array{...}public function testTransformReportResultsWithEmptyCollection0: void{...}-221— 229229=2302312253public function testTransformReportResultsStructure@: void{...}235public function testTransformReportResultsWithMultipleResults@: voidf...}237#DataProvider(isuserRecioient0fReportDataProviden'ii239public function testIsUserRecipient0fReport(int $userId, array Srecipients, bool $expected): voidf,40— 240#[DataProvider('isUserRecipient0fAskJiminnyReportDataProvider')]nublie function testIsllserRecinient0fAsk.liminnvRenontViaGroun^lint Susentd.2int SarounTd.array Srecipients,array SreportGroups,bool $expected,): void {...}243=245=246public static function isUserRecipient0fAskJiminnyReportDataProvider(= arnavfSo jiminny vm 021 A1 A18 V2 V6 ^ Vreports where id = 67;reports where id = 42;E id = 143: # group 28e id = 3143urations where id = 500:e name = 'Integration Account': # 1695ounts WHERE sociable id = 1695:where crm_configuration_ id = 39ecorded' and duration > 60and actual start time >= 12025-12-01'-WHERE uuid to bind'458cf915-6914-4000-6083-5687632629561 = uuid:WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003WHERF id TN (16.422003)-whenp status = Ifailedt.PE activitv id = 422003•Anconid = n.idf1acb-16b8-42d1-8726-df52979dad0e') = U.uuidNULLIacks tid = a.id"video')time DESdeid = 19.unations where nroviden = Ininednive!.a1 secnunte caSocket fail to connect to host:address=(host=localhost) (port=3306)(tvpe=primarv). Connection refused100% 5• Tue 19 May 15:44:39+0 ..CascadeFix AutomatedReportsCascadeText Relay Duplicationcan i just add catch-all-eu and keep catch-all? also cannot I use jiminny.com domain? Just a questionYes, you can keep both email addresses in the same domain:current setup:• catch-allatxt.11minny.com - used by both us and zu (causina duplicates)Proposed setup• [EMAIL] - used by US only• [EMAIL] - used by EU onlyYou would1. Create the catch-all-euatxt.iminny.com email address in Gmail2. Update the code to filter by the specific recipient email instead of just domain3. Update EU contig to use catch-all-eu mailboxReaardina usina liminnv.com domainYes, you could use the main domain instead of the txt.jiminny.com subdomain:catch-allajiminny.com for US• [EMAIL] for EUhowever, this would require• Changing the existing txt.jiminny.com setup (which is currently working for US)• Micht aftect existina users who have SMS nottications with @txt.liminnv.com reolv-to addresses• Would require updating the regex in EmailTextRelay.php to match jiminny.com instead of txt.jiminny.comRecommendation: Keep the txt.liminnv.com domain and just add [EMAIL]. This is the least disruptivechange - you're just adaing a new emall address, not changing the domain structure.Would vou like me to imolement the email-based filterina aporoach usina [EMAIL]?1 ***« Code SWF-1.6WN Windsurf Teamsio 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58500
|
2072
|
4
|
2026-05-19T12:44:39.821182+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194679821_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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}]...
|
-6966022010367698874
|
-564708849257816637
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1DEV (docker)₴82APP (-zslCounting objects: 100% (2/2), done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),reused 0 (delta 0), pack-reusedUnpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objects ->origin/JY-20676-dele*Merge made by theroutes/api.php 17++++---1 file changed, 4 insertions(+),3 deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.Cuse"git push"to publish your local commits)Changes notstaged for commit:(use"git add<file>..."to update what will becommitted)Cuse "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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges added to commit (use "gitadd" and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24, done.Counting objects: 100% (18/18), done.Deltacompression using up to 8 threadsCompressing objects: 100% (9/9), done.Totin1o baects: 7, % s/10 ((e.e1 K), pack-rause/s, done.remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•MoreED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesRo Nikolay YankovD. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Apps|Jira CloudToast> 0.all100% <73• Tue 19 May 15:44:39Describe what you are looking forToastHomeMessagesAbout@Vasil VasilevYesterday~approved by youToday ~Toast APP 10:00 AMMergeapp#12092 Allow owner's role to beselected when setting up a trial19 hours old • 6 files changedAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubReview Toast APP 11:51 AM#518 JY-20919: Lower exee reports batch size by@Steliyan Georgievmerged by @Steliyan Georgiev (author) after approval bynikolaybiaivanovMessage Toast+Aa...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58473
|
2071
|
0
|
2026-05-19T12:37:45.521068+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194265521_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - ...
|
[{"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-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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":"14","depth":4,"bounds":{"left":0.37466756,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.3863032,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"73","depth":4,"bounds":{"left":0.40026596,"top":0.15003991,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\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 = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\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 = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.42652926,"top":0.9577015,"width":0.29321808,"height":0.013567438},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","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}]...
|
7285657229550227811
|
-1412888448243747103
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - ...
|
58471
|
NULL
|
NULL
|
NULL
|
|
58472
|
2070
|
0
|
2026-05-19T12:37:40.909153+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194260909_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - ...
|
[{"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-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"73","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 Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"21","depth":4,"on_screen":true,"role_description":"text"}]...
|
7285657229550227811
|
-1412888448243747103
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - ...
|
58470
|
NULL
|
NULL
|
NULL
|
|
58471
|
NULL
|
0
|
2026-05-19T12:37:11.769237+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194231769_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - ...
|
[{"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-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.098071806,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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":"14","depth":4,"bounds":{"left":0.37466756,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.3863032,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"73","depth":4,"bounds":{"left":0.40026596,"top":0.15003991,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\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 = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\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 = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","depth":3,"bounds":{"left":0.42652926,"top":0.9577015,"width":0.29321808,"height":0.013567438},"on_screen":true,"value":"Socket fail to connect to host:address=(host=localhost)(port=3306)(type=primary). Connection refused","role_description":"text field","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}]...
|
7285657229550227811
|
-1412888448243747103
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58470
|
NULL
|
0
|
2026-05-19T12:37:10.673537+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194230673_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - ...
|
[{"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-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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":"14","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"73","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 Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","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}]...
|
7285657229550227811
|
-1412888448243747103
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20676-delete-report-re Project: faVsco.js, menu
JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => 'Report - ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58469
|
2069
|
16
|
2026-05-19T12:36:39.394438+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194199394_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormProiectcodeFV faVsco.js?9 JY-20676-delete- PhostormProiectcodeFV faVsco.js?9 JY-20676-delete-report-related-objectscustom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy© ResolveCompanyNameByEmailTraitTe© AskAnythingPromptService.php© TimePerioditeratorTest.php© AskAnythingPromptDto.php© UpdateCrmDataResolverTest.php› Internal© AskJiminnyReportsController.phgv C Kioskv _ Automated ReportsC ActivitvTvpeServiceTest.oho© AutomatedReportsServiceTest.php X©) AskAnythingPromptServiceTest.php© Search.phpclass AutomatedReportsservicelest extends Testcase(C) AskJiminnvReoortActivitvServiceTel(C) AskJiminnvReoortServiceTest.ono(C) AutomatedReportsCallbackServicel(C) AutomatedReportsServiceActivities(C) AutomatedReportsServiceExtendedc) AutomatedRenortsServiceSieldbat:(c) AutomatedRenortsServiceSormattin(C) AutomatedRenortsServiceGetActivi© AutomatedReportsServiceGetPlayb©) AutomatedReportsServiceReportGe© AutomatedReportsServiceTest.php(eh AutamatodDonortcCorvicoValidator190 % >(eh AutamatodDonortcCorvicoValidatioi© RecipientServiceTest.php› C Mai> C MeetingGenerator> 0 Notification[ RecallAlD Team_ lelephony201 %UserPillot1 Webhook©) ActivitvServiceTest.ohoC ApiResponseServiceTest.phpC InstantMeetinaServiceTest.pho@* IntercomServiceTest.php@ KioskServiceTest.ohnC PlanhatService Test.nho@ PlavbackVideoOnlyServiceTest.php© PlaybookCategoryServiceTest.php(c) ResolveTeamCrmConnectionTest.nho© SlackServiceTest.phpM CocialA ccountCorviceTact nhn247 %257 %297 Yl© TeamDeactivatedServiceTest.php© TeamOwnerServiceTest.php© TeamServiceTest.php© UserServiceTest.php> Ca Traits> C UseCases> C Usen> C Utils> D ValidationA14 A102 У73 ^ v 208209#DataProvider('transformMed1alypesDataProv1der')Jpublic function testTransformMediaTypes(array SmediaTypes, array Sexpected): void{...}÷8. 221212public function testGetMediaTypeFieldDataWithoutReport(): voidf...}public function testGetMediaTypeFieldDataWithReport(): void{...}1 usagepublic static function transformMediaTvnesDataProvider(): arravf...}#[DataProvider('hasCallTypeConferenceDataProvider')]public function testHasCallTypeConference(array $callTypes, bool Sexpected): void{...}#[DataProvider('hasCallTypeDialerDataProvider')]public function testHasCallTypeDialer(array $callTypes, bool $expected): void{...}— 218=219= 220221=222_2231 usagepublic static function hasCallTypeConferenceDataProvider(): array{...}-221— 229public static function hasCallTypeDialerDataProvider@: array{...}l= 23A231public function testTransformReportResultsWithEmptyCollection0: void{...}public function testTransformReportResultsStructure@: void{...}235public function testTransformReportResultsWithMultipleResults@: voidf...}#DataProvider(isuserRecioient0fReportDataProviden'ii239public function testIsUserRecipient0fReport(int $userId, array $recipients, bool $expected): voidf...#[DataProvider('isUserRecipient0fAskJiminnyReportDataProvider')]nublie function testIsllserRecinient0fAsk.liminnvRenontViaGroun^l243int Susentd.2int SarounTd.=245array Srecipients,=246array SreportGroups,bool $expected,): void {...}Tx: AutovSo jiminny vm 021 A1 A18 V2 Y6 ^:ELECT * FROM users WHERE id = 143: # group 28ielect * from teams where id = 3143:ielect * from crm_confiqurations where id = 500:¡elect * from users where name = 'Integration Account': # 1695BELECT * FROM social accounts WHERE sociable id = 1695:ielect * from activities where crm configuration id = 39ind recordina state = 'recorded' and duration > 60ind status = 'completed' and actual start time >= 12025-12-01'•relect * from leads:GELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003PELECT * EROM activities WHERE id TM (16.422003) •PFLECT * EROM activitios whene status = Ifailedi.activitv id = 422003-PSISCTIOTM ncone m1 nc-s1• 0M a ncon id = midJHEpSactatue - IeomnlotodiAND uuid to_ bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAMn a dolotod o+ Te MulltAND EXISTSSELECT 1 FROM tracks +AND +.tvpe IN ('audio', 'video')FLEC100% 5• Tue 19 May 15:36:38+0 ..CascadeFix AutomatedReportsCascadeText Relay Duplication• catch-all-usatxt.11m1nny.com Tor Us environment•catch-all-euatxt.iiminnv.com for SU environmentbenerits or this approach* No new domain registration needed• No DNS changes required.• Just create new Gmail email addresses in the same domain• Simpler intrastructurewould need to modify the implementation to filter by full email address instead of just domainTextRelayService.php - change from:=phpprivare runccion isrorcurrencenvironmenc(coogleumall sservice, scring smalloox, surino smessagelo, scr.// ... checks if recipient ends with '@' . SexpectedDomain8 nhnlprivate function isForCurrentEnvironment(GoogleGmail $service, string $mailbox, string $messageld, strirtrySmessage = $service->users messages->get(Smailbox, Smessageld):foreach (Sheaders as $header) {if (Sheader-›name === 'To')‹srecinient = Sheader_svalnereturn Srecipient === SexpectedEmail:catch Exception Se) !\Sentry: : captureException($e);recurn Talse:• US: GOOGIF TEYT RFLAY MATIROX-catch-alleuc• EU: GOOGLE TEXT RELAY MAILBOX=catch-all-eu• Both keen: GOOGLE TEXT RELAY HOST-tyt.Biminnv.comWould you like me to implement this email-based filtering instead? It's much simpler to set up.can iiust add catch-all-eu and keep+ @Code SWF-16WN Windsurf TeamsUTE.8io 4 spaces...
|
NULL
|
5717215583110244561
|
NULL
|
click
|
ocr
|
NULL
|
PhostormProiectcodeFV faVsco.js?9 JY-20676-delete- PhostormProiectcodeFV faVsco.js?9 JY-20676-delete-report-related-objectscustom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euy© ResolveCompanyNameByEmailTraitTe© AskAnythingPromptService.php© TimePerioditeratorTest.php© AskAnythingPromptDto.php© UpdateCrmDataResolverTest.php› Internal© AskJiminnyReportsController.phgv C Kioskv _ Automated ReportsC ActivitvTvpeServiceTest.oho© AutomatedReportsServiceTest.php X©) AskAnythingPromptServiceTest.php© Search.phpclass AutomatedReportsservicelest extends Testcase(C) AskJiminnvReoortActivitvServiceTel(C) AskJiminnvReoortServiceTest.ono(C) AutomatedReportsCallbackServicel(C) AutomatedReportsServiceActivities(C) AutomatedReportsServiceExtendedc) AutomatedRenortsServiceSieldbat:(c) AutomatedRenortsServiceSormattin(C) AutomatedRenortsServiceGetActivi© AutomatedReportsServiceGetPlayb©) AutomatedReportsServiceReportGe© AutomatedReportsServiceTest.php(eh AutamatodDonortcCorvicoValidator190 % >(eh AutamatodDonortcCorvicoValidatioi© RecipientServiceTest.php› C Mai> C MeetingGenerator> 0 Notification[ RecallAlD Team_ lelephony201 %UserPillot1 Webhook©) ActivitvServiceTest.ohoC ApiResponseServiceTest.phpC InstantMeetinaServiceTest.pho@* IntercomServiceTest.php@ KioskServiceTest.ohnC PlanhatService Test.nho@ PlavbackVideoOnlyServiceTest.php© PlaybookCategoryServiceTest.php(c) ResolveTeamCrmConnectionTest.nho© SlackServiceTest.phpM CocialA ccountCorviceTact nhn247 %257 %297 Yl© TeamDeactivatedServiceTest.php© TeamOwnerServiceTest.php© TeamServiceTest.php© UserServiceTest.php> Ca Traits> C UseCases> C Usen> C Utils> D ValidationA14 A102 У73 ^ v 208209#DataProvider('transformMed1alypesDataProv1der')Jpublic function testTransformMediaTypes(array SmediaTypes, array Sexpected): void{...}÷8. 221212public function testGetMediaTypeFieldDataWithoutReport(): voidf...}public function testGetMediaTypeFieldDataWithReport(): void{...}1 usagepublic static function transformMediaTvnesDataProvider(): arravf...}#[DataProvider('hasCallTypeConferenceDataProvider')]public function testHasCallTypeConference(array $callTypes, bool Sexpected): void{...}#[DataProvider('hasCallTypeDialerDataProvider')]public function testHasCallTypeDialer(array $callTypes, bool $expected): void{...}— 218=219= 220221=222_2231 usagepublic static function hasCallTypeConferenceDataProvider(): array{...}-221— 229public static function hasCallTypeDialerDataProvider@: array{...}l= 23A231public function testTransformReportResultsWithEmptyCollection0: void{...}public function testTransformReportResultsStructure@: void{...}235public function testTransformReportResultsWithMultipleResults@: voidf...}#DataProvider(isuserRecioient0fReportDataProviden'ii239public function testIsUserRecipient0fReport(int $userId, array $recipients, bool $expected): voidf...#[DataProvider('isUserRecipient0fAskJiminnyReportDataProvider')]nublie function testIsllserRecinient0fAsk.liminnvRenontViaGroun^l243int Susentd.2int SarounTd.=245array Srecipients,=246array SreportGroups,bool $expected,): void {...}Tx: AutovSo jiminny vm 021 A1 A18 V2 Y6 ^:ELECT * FROM users WHERE id = 143: # group 28ielect * from teams where id = 3143:ielect * from crm_confiqurations where id = 500:¡elect * from users where name = 'Integration Account': # 1695BELECT * FROM social accounts WHERE sociable id = 1695:ielect * from activities where crm configuration id = 39ind recordina state = 'recorded' and duration > 60ind status = 'completed' and actual start time >= 12025-12-01'•relect * from leads:GELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003PELECT * EROM activities WHERE id TM (16.422003) •PFLECT * EROM activitios whene status = Ifailedi.activitv id = 422003-PSISCTIOTM ncone m1 nc-s1• 0M a ncon id = midJHEpSactatue - IeomnlotodiAND uuid to_ bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAMn a dolotod o+ Te MulltAND EXISTSSELECT 1 FROM tracks +AND +.tvpe IN ('audio', 'video')FLEC100% 5• Tue 19 May 15:36:38+0 ..CascadeFix AutomatedReportsCascadeText Relay Duplication• catch-all-usatxt.11m1nny.com Tor Us environment•catch-all-euatxt.iiminnv.com for SU environmentbenerits or this approach* No new domain registration needed• No DNS changes required.• Just create new Gmail email addresses in the same domain• Simpler intrastructurewould need to modify the implementation to filter by full email address instead of just domainTextRelayService.php - change from:=phpprivare runccion isrorcurrencenvironmenc(coogleumall sservice, scring smalloox, surino smessagelo, scr.// ... checks if recipient ends with '@' . SexpectedDomain8 nhnlprivate function isForCurrentEnvironment(GoogleGmail $service, string $mailbox, string $messageld, strirtrySmessage = $service->users messages->get(Smailbox, Smessageld):foreach (Sheaders as $header) {if (Sheader-›name === 'To')‹srecinient = Sheader_svalnereturn Srecipient === SexpectedEmail:catch Exception Se) !\Sentry: : captureException($e);recurn Talse:• US: GOOGIF TEYT RFLAY MATIROX-catch-alleuc• EU: GOOGLE TEXT RELAY MAILBOX=catch-all-eu• Both keen: GOOGLE TEXT RELAY HOST-tyt.Biminnv.comWould you like me to implement this email-based filtering instead? It's much simpler to set up.can iiust add catch-all-eu and keep+ @Code SWF-16WN Windsurf TeamsUTE.8io 4 spaces...
|
58467
|
NULL
|
NULL
|
NULL
|
|
58468
|
2068
|
12
|
2026-05-19T12:36:39.392177+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779194199392_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1 SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1DEV (docker)₴82APP (-zslCounting objects: 100% (2/2), done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),reused 0 (delta 0), pack-reusedUnpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objects ->origin/JY-20676-dele*Merge made by theroutes/api.php 17++++---1 file changed, 4 insertions(+),3 deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.Cuse"git push"to publish your local commits)Changes notstaged for commit:(use"git add<file>..."to update what will becommitted)Cuse "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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges added to commit (use "gitadd" and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24, done.Counting objects: 100% (18/18), done.Deltacompression using up to 8 threadsCompressing objects: 100% (9/9), done.Totin1o baects: 7, % s/10 ((e.e1 K), pack-rause/s, done.remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•MoreED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesRo Nikolay YankovD. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Apps|Jira CloudToast> 0.all100% <73• Tue 19 May 15:36:39Describe what you are looking forToastHomeMessagesAbout@Vasil VasilevYesterday~approved by youToday ~Toast APP 10:00 AMMergeapp#12092 Allow owner's role to beselected when setting up a trial19 hours old • 6 files changedAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubReview Toast APP 11:51 AM#518 JY-20919: Lower exee reports batch size by@Steliyan Georgievmerged by @Steliyan Georgiev (author) after approval bynikolaybiaivanovMessage Toast+Aa...
|
NULL
|
-2330933014068880529
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1 SlackFileEditViewGoHistoryWindowHelp•APPDOCKER• ₴1DEV (docker)₴82APP (-zslCounting objects: 100% (2/2), done.Compressing objects: 100% (2/2),remote: Total 2 (delta 0),reused 0 (delta 0), pack-reusedUnpacking objects: 100% (2/2), 2.59 KiB | 1.29 MiB/s, done.From github.com:jiminny/app478e5b33d8..6ecd5a5ff7JY-20676-delete-report-related-objects ->origin/JY-20676-dele*Merge made by theroutes/api.php 17++++---1 file changed, 4 insertions(+),3 deletions(-)lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obOn branch JY-20676-delete-report-related-objectsYour branch isahead of 'origin/JY-20676-delete-report-related-objects' by 2 commits.Cuse"git push"to publish your local commits)Changes notstaged for commit:(use"git add<file>..."to update what will becommitted)Cuse "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.sqltests/Unit/Policies/CanAccessAiReportsTest.phpnochanges added to commit (use "gitadd" and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obEnumerating objects: 24, done.Counting objects: 100% (18/18), done.Deltacompression using up to 8 threadsCompressing objects: 100% (9/9), done.Totin1o baects: 7, % s/10 ((e.e1 K), pack-rause/s, done.remote: Resolving deltas: 100% (7/7), completed with 6 local objects.To github.com:jiminny/app.git6ecd5a5ff7..56ca84fabdJY-20676-delete-report-related-objects -> JY-20676-delete-reporlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-obHomeDMsActivityFilesLater..•MoreED→Jiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messagesRo Nikolay YankovD. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelova8. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan Tanev. Lukas Kovalik y...::: Apps|Jira CloudToast> 0.all100% <73• Tue 19 May 15:36:39Describe what you are looking forToastHomeMessagesAbout@Vasil VasilevYesterday~approved by youToday ~Toast APP 10:00 AMMergeapp#12092 Allow owner's role to beselected when setting up a trial19 hours old • 6 files changedAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubReview Toast APP 11:51 AM#518 JY-20919: Lower exee reports batch size by@Steliyan Georgievmerged by @Steliyan Georgiev (author) after approval bynikolaybiaivanovMessage Toast+Aa...
|
58466
|
NULL
|
NULL
|
NULL
|